mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-20 14:10:44 +00:00
Compare commits
563 Commits
snapshot-f
...
version-0.
Author | SHA1 | Date | |
---|---|---|---|
60096dde93 | |||
9949e96832 | |||
868dcf00d7 | |||
ed6825ff05 | |||
e33033fb05 | |||
32fde00b96 | |||
2491523a6b | |||
6d0f5d4fb3 | |||
bc76a7f66f | |||
5045627b18 | |||
af4dd2a771 | |||
595190fd8f | |||
b8191927e8 | |||
182ffe2939 | |||
ce2ca3f413 | |||
625ed5209c | |||
e603818c69 | |||
fa4cf8ca16 | |||
e58190f15d | |||
be16d5f0cf | |||
e5987238e6 | |||
f81b257dd4 | |||
97434ebb66 | |||
1b9d93e924 | |||
78892b3642 | |||
64a405cbf8 | |||
2d89dafdc1 | |||
28b608c814 | |||
9056be056e | |||
a4ffe815b5 | |||
3f75add3ed | |||
6f1e9413f6 | |||
af51641d2a | |||
17eab1f3d4 | |||
2cd96fd4bc | |||
73aaee1960 | |||
fd13b00793 | |||
64278e674b | |||
2fa829658f | |||
8f751d81fe | |||
d42b67582a | |||
942134b4b0 | |||
aca8436c56 | |||
94631585ee | |||
96c539b323 | |||
235cf9e16c | |||
9cf25ed45c | |||
473cc9c0ae | |||
d733ce9566 | |||
491998ec9a | |||
86997528bb | |||
ebd9dc9c2c | |||
12e194fc2e | |||
55d046cd86 | |||
16cedfeb6e | |||
d0508c2568 | |||
7af23e52d6 | |||
133e507c93 | |||
513c8f2efb | |||
2016d0a448 | |||
1c54a9a8ac | |||
a95650a0ce | |||
410175a133 | |||
8f0d117b13 | |||
916c8c3ba6 | |||
aca199e91e | |||
4acc185698 | |||
d033d3f464 | |||
ae4e269b88 | |||
38cdb94876 | |||
e4d52a0443 | |||
116a4e883b | |||
36ff5eea02 | |||
75fdff031a | |||
14398d2c94 | |||
9aa25b4f14 | |||
b863d62688 | |||
08ed9a5aad | |||
244239f053 | |||
0f8ad2fb78 | |||
f99c39e0f6 | |||
2c32c5a9fc | |||
5a5d98a3ff | |||
7d2282ebb7 | |||
8e9954c909 | |||
8cf53e045e | |||
955cb67a2c | |||
45a6c3bcad | |||
d0768a6981 | |||
0c72210e8e | |||
a2b36a10e7 | |||
f92ab85c8c | |||
2c976e59f4 | |||
23e87cd9ce | |||
7c39372153 | |||
491fcd6d25 | |||
e0045bb212 | |||
04e13413ef | |||
a489808847 | |||
66202b9d02 | |||
1a5a374c4e | |||
51c588d2de | |||
e3c89a80ca | |||
bb79a31b4f | |||
f49be5ff63 | |||
83c4034d90 | |||
3a8bfe5f48 | |||
d4d20e2b20 | |||
4cbcd0a149 | |||
4b0d093a4a | |||
99e282fcdf | |||
9fb53af0ba | |||
7efd8264fa | |||
425d18e6df | |||
2808b3fcc4 | |||
3ae5f90cc8 | |||
92f0bd3846 | |||
2a0bc03337 | |||
b87fb6bd4a | |||
1354755372 | |||
2703f06b23 | |||
428832f4ec | |||
27c816d627 | |||
366f5d9600 | |||
4b585cde45 | |||
eae24e3eba | |||
cdc6df2140 | |||
21c4434831 | |||
b984959aa7 | |||
a0d12eade7 | |||
77cc272425 | |||
80c286ab00 | |||
35c4049174 | |||
cddc42db5b | |||
2b9181b22e | |||
46a1a77d1b | |||
6fe04e65df | |||
001860fc91 | |||
f4f2f3c80e | |||
0395e0d02c | |||
df530b70b8 | |||
693dfb2d5b | |||
e2b81e46b9 | |||
b2c8c075fd | |||
3114812e34 | |||
a659dc7a02 | |||
05bb0ef363 | |||
d401cc9ae1 | |||
44236f4309 | |||
d25eb6ee48 | |||
be4f3c68f0 | |||
2502d102d9 | |||
dcf7218abf | |||
bb1a43f11f | |||
e5bdf8cd5e | |||
31a586f23e | |||
91aa8faca2 | |||
154c5442af | |||
7ab19ca9b4 | |||
dc25f6b289 | |||
2fd3ed2561 | |||
0005c51ecd | |||
143ed927fa | |||
8816177df8 | |||
be6d1b888f | |||
ead66385be | |||
5d2adb76e0 | |||
3f8c2957d1 | |||
c341708d21 | |||
b93114a9ba | |||
5a7199495d | |||
2591e6ad44 | |||
72e175e1a6 | |||
243cf66dbd | |||
7bd560b7a8 | |||
bacfa57c5e | |||
4c502df328 | |||
be25fc1272 | |||
f0c07caba8 | |||
ab31067e11 | |||
a18ffaa3ec | |||
29e407b782 | |||
c47c39f5e7 | |||
cd40177569 | |||
eb8fa42fb8 | |||
9072ce2426 | |||
ec713553b8 | |||
53f48331db | |||
c53f9b8fc9 | |||
74e755b111 | |||
227d22ed14 | |||
7e50860983 | |||
ee4f9bc209 | |||
c76a5e20e8 | |||
e25ec61731 | |||
33f3ebd4c6 | |||
036b02bb6c | |||
1cec2505c9 | |||
54ff797b5d | |||
1f416fcc43 | |||
40b4b55bf4 | |||
f86b3d9973 | |||
2031e05117 | |||
38a17b2954 | |||
a5c65b9e67 | |||
48fbb3d054 | |||
bcca710316 | |||
6d749e9df6 | |||
f0eeb260e3 | |||
d14662e2fc | |||
0635a9128c | |||
81ffe1a286 | |||
6b49abb749 | |||
efb47b9cd6 | |||
29f2feb321 | |||
3537d2cfd1 | |||
634abe3822 | |||
93c7fbca25 | |||
ea40197893 | |||
38293b81d7 | |||
7b141c3f5b | |||
502095002c | |||
42a8d37493 | |||
ccb51be4f9 | |||
b6623fbed0 | |||
fdebdb1095 | |||
8e24faa975 | |||
3013e5dccd | |||
6725704b13 | |||
48ac6a0a4f | |||
3f4d11b22a | |||
c91a7903ba | |||
109e390bc3 | |||
d6288eee4a | |||
8c7e523e43 | |||
0fffed9d31 | |||
84d41858b2 | |||
459629b449 | |||
64de5c9913 | |||
68f9bb20f7 | |||
4b904471af | |||
f4b54518fa | |||
e012b1f090 | |||
1fae1c5e2a | |||
b8ef480804 | |||
b397c4da08 | |||
e2c7748a4b | |||
70b569c2ca | |||
20332fa011 | |||
387804acff | |||
5ad4216434 | |||
f7cbf9d1c2 | |||
bcedb566ff | |||
5171af1c95 | |||
f54b2b79db | |||
d63cff8c5b | |||
32a8d65a84 | |||
62bf361e36 | |||
86bf82f590 | |||
80b24e6dfc | |||
8601347d97 | |||
37aaea3452 | |||
719be86e94 | |||
009e144361 | |||
8e65255248 | |||
2260fbde84 | |||
db1269824c | |||
dc6d37aad3 | |||
aba5b9c5ec | |||
b64efd0246 | |||
6a5f8fadad | |||
5ecae928ac | |||
8d108b671a | |||
f9cd4373aa | |||
e9fc5f81d2 | |||
3fda1a1eda | |||
048ee2e332 | |||
21982e8f53 | |||
8b00e8c877 | |||
f57df2be86 | |||
7f67eda2e3 | |||
a4499219c8 | |||
9cfc7fafc1 | |||
6b7d3ac26d | |||
7e475e2c18 | |||
2b0b176ced | |||
db526009d2 | |||
a6001af7b5 | |||
f147516e45 | |||
f3fe8a3c73 | |||
71dcf231db | |||
a20efabcf2 | |||
00b72e0338 | |||
b979e6545a | |||
7982cad794 | |||
891bdf68b6 | |||
b02818764b | |||
9e348b9817 | |||
000226c30a | |||
cbde8d79bd | |||
3e69003ba7 | |||
d5ec117d1b | |||
edf248c851 | |||
11ff517769 | |||
eba6dfe1b3 | |||
c5f41a8042 | |||
23e730f566 | |||
ec74649c96 | |||
16f931cd5c | |||
d2c0ad498f | |||
5070f0a738 | |||
21a5c98376 | |||
edec6d64e3 | |||
c3c82cbd4a | |||
6687a58bfa | |||
96761b7162 | |||
7bdea734b4 | |||
abc6331131 | |||
e84fe7eb18 | |||
63a48eeafa | |||
5434721c8e | |||
271f2dc25b | |||
c4583f16a9 | |||
434d158776 | |||
eec1924113 | |||
164d9e1de5 | |||
131da68a38 | |||
f7bd049b81 | |||
76d7a8a858 | |||
8d37ce3c54 | |||
7bab11ea7e | |||
8157510c04 | |||
b5eae02fa4 | |||
1911e27cc0 | |||
21aeac2def | |||
2bf12158be | |||
7e3592628a | |||
21069e2310 | |||
11db820196 | |||
a7247b5970 | |||
7cd3105ee6 | |||
86f8e24d5f | |||
b0cc93cbb7 | |||
b055913fc8 | |||
0e93b90270 | |||
8ec6ccd691 | |||
53ca77cde6 | |||
a439bffc69 | |||
8ea16db1fc | |||
61582680f3 | |||
8c6b4e6863 | |||
9213b8987b | |||
c88fd5b7d4 | |||
6ed9dfd498 | |||
17fc976877 | |||
3b24cb745c | |||
6672f95987 | |||
1c2638a5c4 | |||
c883749ba9 | |||
3c06e0e589 | |||
bdbb2d2d00 | |||
58ae17bbac | |||
f3546da8cc | |||
cfd3100535 | |||
0dbac39ef5 | |||
00b4708d80 | |||
b5959b4b89 | |||
243ffe81a5 | |||
76118bfca1 | |||
6e91149b0a | |||
cfeb71aa2f | |||
edaabc3523 | |||
e53e00c520 | |||
e970d613a7 | |||
f5c1573102 | |||
2103d578b3 | |||
daad8a720a | |||
0ef01efcaa | |||
9ad9d52634 | |||
07c0413277 | |||
2918235f46 | |||
07886214f5 | |||
22ce5acf46 | |||
d8ac14a756 | |||
b684f2409b | |||
c09198eed5 | |||
062240a0a5 | |||
6aafc3d553 | |||
39b322336f | |||
4b590b5653 | |||
da2be57a17 | |||
5f081fce44 | |||
e809c773f9 | |||
d8a0a6c68d | |||
7ba205a262 | |||
7d058530d5 | |||
73200b2fd2 | |||
389f0ad16f | |||
6ef0a89533 | |||
ce50120234 | |||
50ef9420f6 | |||
c9fefb45a5 | |||
bf97d757b0 | |||
c481940736 | |||
5f0f4cdab3 | |||
8356fc3f12 | |||
8534a2ca55 | |||
46e54ed8e3 | |||
45439d7596 | |||
f01301e993 | |||
59edb34c12 | |||
74ea6a2d90 | |||
93ab08cbf1 | |||
a5051e559a | |||
c5fdfceae6 | |||
2965338e22 | |||
5e37beabbe | |||
428e188b4b | |||
533822973b | |||
5e9bd2a7e7 | |||
7c54006985 | |||
678ecfd589 | |||
cc55b32206 | |||
8dedc98866 | |||
cde7a60ae0 | |||
aef366e5fe | |||
a46397df39 | |||
8500a2559c | |||
872e125810 | |||
eb754b42b9 | |||
9ea92553e7 | |||
26a33eb1a0 | |||
e40b35154e | |||
8436f2ab0a | |||
7beea514d2 | |||
fc23718c4f | |||
efe89c7043 | |||
bbf4c2c2ff | |||
ff1e022798 | |||
f09735c811 | |||
7ab9171998 | |||
b979f413c8 | |||
766881dee0 | |||
f65b16df60 | |||
e0597827ef | |||
10014f16ae | |||
526ba6ca30 | |||
4f92fb2ae2 | |||
b687d07e46 | |||
b955a20e18 | |||
eb8781db77 | |||
febda51233 | |||
27dbc72db4 | |||
983a93d38c | |||
28ad0661d1 | |||
a8e235c155 | |||
c96bb9dda8 | |||
e4bef88406 | |||
c18aa44010 | |||
47e95d74e3 | |||
cf4c6d2144 | |||
780341b5cc | |||
20d4a9ffeb | |||
4f1310ded9 | |||
791b77b938 | |||
e6864b89c1 | |||
c3171c335f | |||
bb548b78d9 | |||
161591405b | |||
3cc0cfd86c | |||
fb16a041fb | |||
9bf9825132 | |||
a7ca34ec92 | |||
403227bae1 | |||
ab4837ff16 | |||
107acb5685 | |||
65166150e6 | |||
c678a8159e | |||
6673a8fc47 | |||
c4f4faf32b | |||
9de08be978 | |||
4349b37c8d | |||
afb6aa3b89 | |||
6c9ce41c7b | |||
dc34e69c3c | |||
f457fd0860 | |||
c3834efad3 | |||
d513c8431b | |||
fc4e69f059 | |||
050208cdda | |||
8f4146923b | |||
666f4a872d | |||
89e0fc566d | |||
42fd5a0cb3 | |||
89cf23a65a | |||
57b0d6c29b | |||
6702c06ed0 | |||
c90def42f5 | |||
9dfbd839c8 | |||
724d5779cc | |||
1fef376e65 | |||
ed1e251934 | |||
81248a8daf | |||
d3417a0652 | |||
053d5f1058 | |||
20a5130757 | |||
47e27d5ffc | |||
59a70a4cb7 | |||
fea757c46d | |||
9a65ea81b2 | |||
494ec00b84 | |||
9b4b61af38 | |||
f237b5e82d | |||
207311eb0b | |||
ab5af234af | |||
9baa7c32bf | |||
3eae3a5758 | |||
a11d584c8a | |||
ba3cf53c30 | |||
d44790545d | |||
5aed59b9b1 | |||
3bcc0a17bc | |||
09c4d99612 | |||
26fc4fb4e0 | |||
d92be4e69b | |||
2de3306f95 | |||
58b0936c50 | |||
51eb7d89be | |||
0b5e97d596 | |||
2609bc801c | |||
583d702355 | |||
06a69279a8 | |||
9a2276edf2 | |||
36307dba24 | |||
fa2b1c0b8e | |||
840e1aada3 | |||
22d5bc547c | |||
b7cfea157d | |||
028751e23a | |||
be0e1f9c0b | |||
912e40fe0b | |||
f9af2ba983 | |||
61ec57af02 | |||
ccce1a3d1f | |||
eb36630bcd | |||
c3f702bb65 | |||
31fa3c3921 | |||
099fd27309 | |||
95998b687b | |||
1a6cc5bf3c | |||
27a6c0d53c | |||
27c693f0c4 | |||
a3433d60f7 | |||
c2a13b1ada | |||
576ca8a6df | |||
a9a988f221 | |||
7f23a0da79 | |||
0d2e6012a3 | |||
bce9af06fb | |||
385f4c20e5 | |||
6b7fb21d76 | |||
172b25f33e | |||
8dbf7fe4cd |
@ -1,23 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$CIRCLE_BRANCH" ] && [ -z "$CIRCLE_TAG" ]; then
|
||||
echo "Error: env vars CIRCLE_BRANCH and CIRCLE_TAG were not set."
|
||||
exit 1;
|
||||
fi
|
||||
############################################################################
|
||||
## adjust-pom.version.sh
|
||||
## During CircleCI builds - edit the qqq parent pom.xml, to set the
|
||||
## <revision> value such that:
|
||||
## - feature-branch builds, tagged as snapshot-*, deploy with a version
|
||||
## number that includes that tag's name (minus the snapshot- part)
|
||||
## - integration-branch builds deploy with a version number that includes
|
||||
## the branch name slugified
|
||||
## - we never deploy -SNAPSHOT versions any more - because we don't believe
|
||||
## it is ever valid to not know exactly what versions you are getting
|
||||
## (perhaps because we are too loose with our versioning?)
|
||||
############################################################################
|
||||
|
||||
if [ "$CIRCLE_BRANCH" == "dev" ] || [ "$CIRCLE_BRANCH" == "staging" ] || [ "$CIRCLE_BRANCH" == "main" ] || [ \! -z $(echo "$CIRCLE_TAG" | grep "^version-") ]; then
|
||||
echo "On a primary branch or tag [${CIRCLE_BRANCH}${CIRCLE_TAG}] - will not edit the pom version.";
|
||||
POM=$(dirname $0)/../pom.xml
|
||||
echo "On branch: $CIRCLE_BRANCH, tag: $CIRCLE_TAG..."
|
||||
|
||||
######################################################################
|
||||
## ## only do anything if the committed pom has a -SNAPSHOT version ##
|
||||
######################################################################
|
||||
REVISION=$(grep '<revision>' $POM | sed 's/.*<revision>//;s/<.*//');
|
||||
echo "<revision> in pom.xml is: $REVISION"
|
||||
if [ \! $(echo "$REVISION" | grep SNAPSHOT) ]; then
|
||||
echo "Not on a SNAPSHOT revision, so nothing to do here."
|
||||
exit 0;
|
||||
fi
|
||||
|
||||
if [ -n "$CIRCLE_BRANCH" ]; then
|
||||
SLUG=$(echo $CIRCLE_BRANCH | sed 's/[^a-zA-Z0-9]/-/g')
|
||||
else
|
||||
SLUG=$(echo $CIRCLE_TAG | sed 's/^snapshot-//g')
|
||||
##################################################################################
|
||||
## ## figure out if we need a SLUG: a snapshot- tag, or an integration/ branch ##
|
||||
##################################################################################
|
||||
SLUG=""
|
||||
if [ $(echo "$CIRCLE_TAG" | grep ^snapshot-) ]; then
|
||||
SLUG=$(echo "$CIRCLE_TAG" | sed "s/^snapshot-//")-
|
||||
echo "Using slug [$SLUG] from tag [$CIRCLE_TAG]"
|
||||
|
||||
elif [ $(echo "$CIRCLE_BRANCH" | grep ^integration/) ]; then
|
||||
SLUG=$(echo "$CIRCLE_BRANCH" | sed "s,/,-,g")-
|
||||
echo "Using slug [$SLUG] from branch [$CIRCLE_BRANCH]"
|
||||
fi
|
||||
|
||||
POM=$(dirname $0)/../pom.xml
|
||||
################################################################
|
||||
## ## build the replcaement for -SNAPSHOT, and update the pom ##
|
||||
################################################################
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
REPLACEMENT=${SLUG}${TIMESTAMP}
|
||||
|
||||
echo "Updating $POM <revision> to: $SLUG-SNAPSHOT"
|
||||
sed -i "s/<revision>.*/<revision>$SLUG-SNAPSHOT<\/revision>/" $POM
|
||||
echo "Updating $POM -SNAPSHOT to: -$REPLACEMENT"
|
||||
sed -i "s/-SNAPSHOT<\/revision>/-$REPLACEMENT<\/revision>/" $POM
|
||||
git diff $POM
|
||||
|
||||
|
@ -2,6 +2,7 @@ version: 2.1
|
||||
|
||||
orbs:
|
||||
localstack: localstack/platform@2.1
|
||||
browser-tools: circleci/browser-tools@1.4.7
|
||||
|
||||
commands:
|
||||
store_jacoco_site:
|
||||
@ -38,6 +39,8 @@ commands:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "pom.xml" }}
|
||||
- browser-tools/install-chrome
|
||||
- browser-tools/install-chromedriver
|
||||
- run:
|
||||
name: Write .env
|
||||
command: |
|
||||
@ -79,6 +82,19 @@ commands:
|
||||
- ~/.m2
|
||||
key: v1-dependencies-{{ checksum "pom.xml" }}
|
||||
|
||||
check_middleware_api_versions:
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "pom.xml" }}
|
||||
- run:
|
||||
name: Build and Run ValidateApiVersions
|
||||
command: |
|
||||
mvn -s .circleci/mvn-settings.xml -T4 install -DskipTests
|
||||
mvn -s .circleci/mvn-settings.xml -pl qqq-middleware-javalin package appassembler:assemble -DskipTests
|
||||
qqq-middleware-javalin/target/appassembler/bin/ValidateApiVersions -r $(pwd)
|
||||
|
||||
mvn_jar_deploy:
|
||||
steps:
|
||||
- checkout
|
||||
@ -114,14 +130,9 @@ commands:
|
||||
command: |
|
||||
cd docs
|
||||
asciidoctor -a docinfo=shared index.adoc
|
||||
|
||||
upload_docs_site:
|
||||
steps:
|
||||
- run:
|
||||
name: scp html to justinsgotskinnylegs.com
|
||||
command: |
|
||||
cd docs
|
||||
scp index.html dkelkhoff@45.79.44.221:/mnt/first-volume/dkelkhoff/nginx/html/justinsgotskinnylegs.com/qqq-docs.html
|
||||
- store_artifacts:
|
||||
path: docs/index.html
|
||||
when: always
|
||||
|
||||
jobs:
|
||||
mvn_test:
|
||||
@ -130,6 +141,7 @@ jobs:
|
||||
## - localstack/startup
|
||||
- install_java17
|
||||
- mvn_verify
|
||||
- check_middleware_api_versions
|
||||
|
||||
mvn_deploy:
|
||||
executor: localstack/default
|
||||
@ -137,6 +149,7 @@ jobs:
|
||||
## - localstack/startup
|
||||
- install_java17
|
||||
- mvn_verify
|
||||
- check_middleware_api_versions
|
||||
- mvn_jar_deploy
|
||||
|
||||
publish_asciidoc:
|
||||
@ -144,7 +157,6 @@ jobs:
|
||||
steps:
|
||||
- install_asciidoctor
|
||||
- run_asciidoctor
|
||||
- upload_docs_site
|
||||
|
||||
workflows:
|
||||
test_only:
|
||||
|
@ -213,18 +213,6 @@
|
||||
<property name="tokens" value="VARIABLE_DEF"/>
|
||||
<property name="allowSamelineMultipleAnnotations" value="true"/>
|
||||
</module>
|
||||
<module name="NonEmptyAtclauseDescription"/>
|
||||
<!-- <module name="JavadocTagContinuationIndentation"/> -->
|
||||
<!--
|
||||
<module name="SummaryJavadoc">
|
||||
<property name="forbiddenSummaryFragments" value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
|
||||
</module>
|
||||
-->
|
||||
<!-- <module name="JavadocParagraph"/> -->
|
||||
<module name="AtclauseOrder">
|
||||
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
|
||||
<property name="target" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
|
||||
</module>
|
||||
<module name="JavadocMethod">
|
||||
<property name="allowMissingParamTags" value="true"/>
|
||||
<property name="allowMissingReturnTag" value="true"/>
|
||||
@ -233,23 +221,14 @@
|
||||
<module name="MissingJavadocMethod">
|
||||
<property name="scope" value="private"/>
|
||||
</module>
|
||||
<module name="MissingJavadocType">
|
||||
<property name="scope" value="private"/>
|
||||
</module>
|
||||
<module name="MethodName">
|
||||
<property name="format" value="^[a-z][a-zA-Z0-9_]*$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Method name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="SingleLineJavadoc">
|
||||
<property name="ignoreInlineTags" value="false"/>
|
||||
</module>
|
||||
|
||||
<module name="MagicNumber">
|
||||
<property name="severity" value="info"/>
|
||||
<property name="tokens" value="NUM_DOUBLE, NUM_FLOAT, NUM_INT"/>
|
||||
<property name="ignoreNumbers" value="0, 1, 2, 3, 4, 5, 6, 7, 8"/>
|
||||
<property name="ignoreFieldDeclaration" value="true"/>
|
||||
<property name="ignoreAnnotation" value="true"/>
|
||||
</module>
|
||||
|
||||
<module name="EmptyCatchBlock">
|
||||
<property name="exceptionVariableName" value="expected"/>
|
||||
</module>
|
||||
|
@ -91,7 +91,7 @@ And then having a bug in the check permission logic on the _Light Bulb Inventory
|
||||
No!
|
||||
|
||||
All of the (really important, even though application developers hate doing it) aspects of security - you don't need to write ANY code for dealing with that.
|
||||
Just tell QQQ what Authentication provider you want to use (e.g., https://auth0.com/[Auth0]), and - to paraphrase the old https://www.youtube.com/watch?v=YHzM4avGrKI[iMac ad] - there's no step 2.
|
||||
Just tell QQQ what Authentication provider you want to use (e.g., OAuth2 or https://auth0.com/[Auth0]), and - to paraphrase the old https://www.youtube.com/watch?v=YHzM4avGrKI[iMac ad] - there's no step 2.
|
||||
QQQ just does it.
|
||||
|
||||
''''
|
||||
@ -136,17 +136,13 @@ This speaks to the fact that this "code" is not executable code - but rather is
|
||||
**** The Filter button in the Query Screen will present a menu listing all fields from the table for the user to build ad-hoc queries against the table.
|
||||
The data-types specified for the fields (in the meta-data) dictate what operators QQQ allows the user to use against fields (e.g., Strings offer "contains" vs Numbers offer "greater than").
|
||||
**** Values for records from the table will be formatted for presentation based on the meta-data (such as a numeric field being shown with commas if it represents a quantity, or formatted as currency).
|
||||
...
|
||||
|
||||
[start=2]
|
||||
. *Meta Data* - declarative code - java object instances (potentially which could be read from `.yaml` files or other data sources in a future version of QQQ), which tell QQQ about the backend systems, tables, processes, reports, widgets, etc, that make up the application.
|
||||
For example:
|
||||
* Details about the database you are using, and how to connect to it.
|
||||
* A database table's name, fields, their types, its keys, and basic business rules (required fields, read-only fields, field lengths).
|
||||
* The description of web API - its URL and authentication mechanism.
|
||||
* A table/path within a web API, and the fields returned in the JSON at that endpoint.
|
||||
* The specification of a custom workflow (process), including what screens are needed, with input & output values, and references to the custom application code for processing the data.
|
||||
* Details about a chart that summarizes data from a table for presentation as a dashboard widget.
|
||||
* Other kinds of information that you tell QQQ about in the form of meta-data objects includes:
|
||||
** Details about the database you are using, and how to connect to it.
|
||||
** A database table's name, fields, their types, its keys, and basic business rules (required fields, read-only fields, field lengths).
|
||||
** The specification of a custom workflow (process), including what screens are needed, with input & output values, and references to the custom application code for processing the data.
|
||||
** Details about a chart that summarizes data from a table for presentation as a dashboard widget.
|
||||
** The description of web API - its URL and authentication mechanism.
|
||||
** A table/path within a web API, and the fields returned in the JSON at that endpoint.
|
||||
// the section below is kinda dumb. like, it says you have to write application code, but
|
||||
// then it just talks about how your app code gets for-free the same shit that QQQ does.
|
||||
// it should instead say more about what your custom app code is or does.
|
||||
@ -164,7 +160,8 @@ For example:
|
||||
// * The multi-threaded, paged producer/consumer pattern used in standard framework actions is how all custom application actions are also invoked.
|
||||
// ** For example, the standard QQQ Bulk Edit action uses the same streamed-ETL process that custom application processes can use.
|
||||
// Meaning your custom processes can take full advantage of the same complex frontend, middleware, and backend structural pieces, and you can just focus on your unique busines logic needs.
|
||||
2. *Application code* - to customize beyond what the QQQ framework does out-of-the box, and to provide application-specific business-logic.
|
||||
|
||||
. *Application code* - to customize beyond what the QQQ framework does out-of-the box, and to provide application-specific business-logic.
|
||||
QQQ provides its programmers the same classes that it internally uses for record access, resulting in a unified application model.
|
||||
For example:
|
||||
|
||||
|
@ -33,9 +33,6 @@ If the {link-table} has a `POST_QUERY_CUSTOMIZER` defined, then after records ar
|
||||
* `table` - *String, Required* - Name of the table being queried against.
|
||||
* `filter` - *<<QQueryFilter>> object* - Specification for what records should be returned, based on *<<QFilterCriteria>>* objects, and how they should be sorted, based on *<<QFilterOrderBy>>* objects.
|
||||
If a `filter` is not given, then all rows in the table will be returned by the query.
|
||||
* `skip` - *Integer* - Optional number of records to be skipped at the beginning of the result set.
|
||||
e.g., for implementing pagination.
|
||||
* `limit` - *Integer* - Optional maximum number of records to be returned by the query.
|
||||
* `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.
|
||||
@ -55,6 +52,14 @@ But if running a query to provide data as part of a process, then this can gener
|
||||
* `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 below for further details.
|
||||
* `fieldNamesToInclude` - *Set of String* - Optional set of field names to be included in the records.
|
||||
** Fields from a queryJoin must be prefixed by the join table's name or alias, and a period.
|
||||
Field names from the table being queried should not have any sort of prefix.
|
||||
** A `null` set here (default) means to include all fields from the table and any queryJoins set as select=true.
|
||||
** An empty set will cause an error, as well any unrecognized field names.
|
||||
** `QueryAction` will validate the set of field names, and throw an exception if any unrecognized names are given.
|
||||
** _Note that this is an optional feature, which some backend modules may not implement.
|
||||
Meaning, they would always return all fields._
|
||||
|
||||
==== QQueryFilter
|
||||
A key component of *<<QueryInput>>*, a *QQueryFilter* defines both what records should be included in a query's results (e.g., an SQL `WHERE`), as well as how those results should be sorted (SQL `ORDER BY`).
|
||||
@ -68,6 +73,9 @@ In general, multiple *orderBys* can be given (depending on backend implementatio
|
||||
** Each *subFilter* can include its own additional *subFilters*.
|
||||
** Each *subFilter* can specify a different *booleanOperator*.
|
||||
** For example, consider the following *QQueryFilter*, that uses two *subFilters*, and a mix of *booleanOperators*
|
||||
* `skip` - *Integer* - Optional number of records to be skipped at the beginning of the result set.
|
||||
e.g., for implementing pagination.
|
||||
* `limit` - *Integer* - Optional maximum number of records to be returned by the query.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@ -147,9 +155,9 @@ new QFilterOrderBy()
|
||||
----
|
||||
|
||||
==== QueryJoin
|
||||
* `joinTable` - *String, required* - Name of the table that is being joined in to the existing query.
|
||||
* `joinTable` - *String, required (though inferrable)* - Name of the table that is being joined in to the existing query.
|
||||
** Will be inferred from *joinMetaData*, if *joinTable* is not set when *joinMetaData* gets set.
|
||||
* `baseTableOrAlias` - *String, required* - Name of a table (or an alias) already defined in the query, to which the *joinTable* will be joined.
|
||||
* `baseTableOrAlias` - *String, required (though inferrable)* - Name of a table (or an alias) already defined in the query, to which the *joinTable* will be joined.
|
||||
** Will be inferred from *joinMetaData*, if *baseTableOrAlias* is not set when *joinMetaData* gets set (which will only use the leftTableName from the joinMetaData - never an alias).
|
||||
* `joinMetaData` - *QJoinMetaData object* - Optional specification of a {link-join} in the current QInstance.
|
||||
If not set, will be looked up at runtime based on *baseTableOrAlias* and *joinTable*.
|
||||
@ -157,21 +165,78 @@ If not set, will be looked up at runtime based on *baseTableOrAlias* and *joinTa
|
||||
* `alias` - *String* - Optional (unless multiple instances of the same table are being joined together, when it becomes required).
|
||||
Behavior based on SQL `FROM` clause aliases.
|
||||
If given, must be used as the part before the dot in field name specifications throughout the rest of the query input.
|
||||
* `select` - *boolean, default: false* - Specify whether fields from the *rightTable* should be selected by the query.
|
||||
* `select` - *boolean, default: false* - Specify whether fields from the *joinTable* should be selected by the query.
|
||||
If *true*, then the `QRecord` objects returned by this query will have values with corresponding to the (table-or-alias `.` field-name) form.
|
||||
* `type` - *Enum of INNER, LEFT, RIGHT, FULL, default: INNER* - specifies the SQL-style type of join being performed.
|
||||
|
||||
[source,java]
|
||||
.QueryJoin definition examples:
|
||||
.Basic QueryJoin usage example:
|
||||
----
|
||||
// selecting from an "orderLine" table - then join to its corresponding "order" table
|
||||
// selecting from an "orderLine" table, joined to its corresponding (parent) "order" table
|
||||
queryInput.withTableName("orderLine");
|
||||
queryInput.withQueryJoin(new QueryJoin("order").withSelect(true));
|
||||
...
|
||||
queryOutput.getRecords().get(0).getValueBigDecimal("order.grandTotal");
|
||||
----
|
||||
|
||||
[source,java]
|
||||
."V" shaped query - selecting from one parent table, and two children joined to it:
|
||||
----
|
||||
// TODO this needs verified for accuracy, though is a reasonable starting point as-is
|
||||
// selecting from an "order" table, and two children of it, orderLine and customer
|
||||
queryInput.withTableName("order");
|
||||
queryInput.withQueryJoin(new QueryJoin("orderLine").withSelect(true));
|
||||
queryInput.withQueryJoin(new QueryJoin("customer").withSelect(true));
|
||||
...
|
||||
QRecord joinedRecord = queryOutput.getRecords().get(0);
|
||||
joinedRecord.getValueString("orderNo");
|
||||
joinedRecord.getValueString("orderLine.sku");
|
||||
joinedRecord.getValueString("customer.firstName");
|
||||
----
|
||||
|
||||
[source,java]
|
||||
."Chain" shaped query - selecting from one parent table, a child table, and a grandchild:
|
||||
----
|
||||
// TODO this needs verified for accuracy, though is a reasonable starting point as-is
|
||||
// selecting from an "order" table, with a "customer" child table, and an "address" sub-table
|
||||
queryInput.withTableName("order");
|
||||
queryInput.withQueryJoin(new QueryJoin("customer").withSelect(true));
|
||||
queryInput.withQueryJoin(new QueryJoin("address").withSelect(true));
|
||||
...
|
||||
QRecord joinedRecord = queryOutput.getRecords().get(0);
|
||||
joinedRecord.getValueString("orderNo");
|
||||
joinedRecord.getValueString("customer.firstName");
|
||||
joinedRecord.getValueString("address.street1");
|
||||
----
|
||||
|
||||
[source,java]
|
||||
.QueryJoin usage example where two tables have two different joins between them:
|
||||
----
|
||||
// TODO this needs verified for accuracy, though is a reasonable starting point as-is
|
||||
// here there's a "fulfillmentPlan" table, which points at the order table (many-to-one,
|
||||
// as an order's plan can change over time, and we keep old plans around).
|
||||
// This join is named: fulfillmentPlanJoinOrder
|
||||
//
|
||||
// The other join is "order" pointing at its current "fulfillmentPlan"
|
||||
// This join is named: orderJoinCurrentFulfillmentPlan
|
||||
|
||||
// to select an order along with its current fulfillment plan:
|
||||
queryInput.withTableName("order");
|
||||
queryInput.withQueryJoin(new QueryJoin(instance.getJoin("orderJoinCurrentFulfillmentPlan"))
|
||||
.withSelect(true));
|
||||
|
||||
// to select an order, and all fulfillment plans for an order (1 or more records):
|
||||
queryInput.withTableName("order");
|
||||
queryInput.withQueryJoin(new QueryJoin(instance.getJoin("fulfillmentPlanJoinOrder"))
|
||||
.withSelect(true));
|
||||
----
|
||||
|
||||
[source,java]
|
||||
.QueryJoin usage example for table with two joins to the same child table, selecting from both:
|
||||
----
|
||||
// given an "order" table with 2 foreign keys to a customer table (billToCustomerId and shipToCustomerId)
|
||||
// Note, we must supply the JoinMetaData to the QueryJoin, to drive what fields to join on in each case.
|
||||
// we must also define an alias for each of the QueryJoins
|
||||
queryInput.withTableName("order");
|
||||
queryInput.withQueryJoins(List.of(
|
||||
new QueryJoin(instance.getJoin("orderJoinShipToCustomer")
|
||||
@ -182,11 +247,18 @@ queryInput.withQueryJoins(List.of(
|
||||
.withSelect(true))));
|
||||
...
|
||||
record.getValueString("billToCustomer.firstName")
|
||||
+ " placed an order for "
|
||||
+ " paid for an order, to be sent to "
|
||||
+ record.getValueString("shipToCustomer.firstName")
|
||||
|
||||
----
|
||||
|
||||
[source,java]
|
||||
.Implicit QueryJoin, where unambiguous and required by QQueryFilter
|
||||
----
|
||||
// TODO finish and verify
|
||||
queryInput.withTableName("order");
|
||||
----
|
||||
|
||||
=== QueryOutput
|
||||
* `records` - *List of QRecord* - List of 0 or more records that match the query filter.
|
||||
** _Note: If a *recordPipe* was supplied to the QueryInput, then calling `queryOutput.getRecords()` will result in an `IllegalStateException` being thrown - as the records were placed into the pipe as they were fetched, and cannot all be accessed as a single list._
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,10 @@
|
||||
|
||||
include::Introduction.adoc[leveloffset=+1]
|
||||
|
||||
== Meta Data
|
||||
== Meta Data Production
|
||||
include::metaData/MetaDataProduction.adoc[leveloffset=+1]
|
||||
|
||||
== Meta Data Types
|
||||
// Organizational units
|
||||
include::metaData/QInstance.adoc[leveloffset=+1]
|
||||
include::metaData/Backends.adoc[leveloffset=+1]
|
||||
@ -28,11 +31,9 @@ include::metaData/PermissionRules.adoc[leveloffset=+1]
|
||||
|
||||
== Services
|
||||
|
||||
include::misc/Javalin.adoc[leveloffset=+1]
|
||||
include::misc/ScheduledJobs.adoc[leveloffset=+1]
|
||||
|
||||
=== Web server (Javalin)
|
||||
#todo#
|
||||
|
||||
=== API server (OpenAPI)
|
||||
#todo#
|
||||
|
||||
|
@ -56,6 +56,37 @@ if the value in the field is longer than the `maxLength`, then one of the follow
|
||||
|
||||
----
|
||||
|
||||
===== ValueRangeBehavior
|
||||
Used on Numeric fields. Specifies min and/or max allowed values for the field.
|
||||
For each of min and max, the following attributes can be set:
|
||||
|
||||
* `minValue` / `maxValue` - the number that is the limit.
|
||||
* `minAllowEqualTo` / `maxAllowEqualTo` - boolean (default true). Controls if < (>) or ≤ (≥).
|
||||
* `minBehavior` / `maxBehavior` - enum of `ERROR` (default) or `CLIP`.
|
||||
** If `ERROR`, then a value not within the range causes an error, and the value does not get stored.
|
||||
** else if `CLIP`, then a value not within the range gets "clipped" to either be the min/max (if allowEqualTo),
|
||||
or to the min/max plus/minus the clipAmount
|
||||
* `minClipAmount` / `maxClipAmount` - Default 1. Used when behavior is `CLIP` (only applies when
|
||||
not allowEqualTo).
|
||||
|
||||
[source,java]
|
||||
.Examples of using ValueRangeBehavior
|
||||
----
|
||||
new QFieldMetaData("noOfShoes", QFieldType.INTEGER)
|
||||
.withBehavior(new ValueRangeBehavior().withMinValue(0));
|
||||
|
||||
new QFieldMetaData("price", QFieldType.BIG_DECIMAL)
|
||||
.withBehavior(new ValueRangeBehavior()
|
||||
// set the min value to be >= 0, and an error if an input is < 0.
|
||||
.withMinValue(BigDecimal.ZERO)
|
||||
.withMinAllowEqualTo(true)
|
||||
.withMinBehavior(ERROR)
|
||||
// set the max value to be < 100 - but effectively, clip larger values to 99.99
|
||||
// here we use the .withMax() method that takes 4 params vs. calling 4 .withMax*() methods.
|
||||
.withMax(new BigDecimal("100.00"), false, CLIP, new BigDecimal("0.01"))
|
||||
);
|
||||
----
|
||||
|
||||
===== DynamicDefaultValueBehavior
|
||||
Used to set a dynamic default value to a field when it is being inserted or updated.
|
||||
For example, instead of having a hard-coded `defaultValue` specified in the field meta-data,
|
||||
|
413
docs/metaData/MetaDataProduction.adoc
Normal file
413
docs/metaData/MetaDataProduction.adoc
Normal file
@ -0,0 +1,413 @@
|
||||
[#MetaDataProduction]
|
||||
include::../variables.adoc[]
|
||||
|
||||
The first thing that an application built using QQQ needs to do is to define its meta data.
|
||||
This basically means the construction of a `QInstance` object, which is populated with the
|
||||
meta data objects defining the backend(s), tables, processes, possible-value sources, joins,
|
||||
authentication provider, etc, that make your application.
|
||||
|
||||
There are various styles that can be used for how you define your meta data, and for the
|
||||
most part they can be mixed and matched. They will be presented here based on the historical
|
||||
evolution of how they were added to QQQ, where we generally believe that better techniques have
|
||||
been added over time. So, you may wish to skip the earlier techniques, and jump straight to
|
||||
the end of this section. However, it can always be instructive to learn about the past, so,
|
||||
read at your own pace.
|
||||
|
||||
== Omni-Provider
|
||||
At the most basic level, the way to populate a `QInstance` is the simple and direct approach of creating
|
||||
one big class, possibly with just one big method, and just doing all the work directly inline there.
|
||||
|
||||
This may (clearly) violate several good engineering principles. However, it does have the benefit of
|
||||
being simple - if all of your meta-data is defined in one place, it can be pretty simple to find where
|
||||
that place is. So - especially in a small project, this technique may be worth continuing to consider.
|
||||
|
||||
Re: "doing all the work" as mentioned above - what work are we talking about? At a minimum, we need
|
||||
to construct the following meta-data objects, and pass them into our `QInstance`:
|
||||
|
||||
* `QAuthenticationMetaData` - how (or if!) users will be authenticated to the application.
|
||||
* `QBackendMeataData` - a backend data store.
|
||||
* `QTableMetaData` - a table (within the backend).
|
||||
* `QAppMetaData` - an organizational unit to present the other elements in a UI.
|
||||
|
||||
Here's what a single-method omni-provider could look like:
|
||||
|
||||
[source,java]
|
||||
.About the simplest possible single-file meta-data provider
|
||||
----
|
||||
public QInstance defineQInstance()
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
|
||||
qInstance.setAuthentication(new QAuthenticationMetaData()
|
||||
.withName("anonymous")
|
||||
.withType(QAuthenticationType.FULLY_ANONYMOUS));
|
||||
|
||||
qInstance.addBackend(new QBackendMetaData()
|
||||
.withBackendType(MemoryBackendModule.class)
|
||||
.withName("memoryBackend"));
|
||||
|
||||
qInstance.addTable(new QTableMetaData()
|
||||
.withName("myTable")
|
||||
.withPrimaryKeyField("id")
|
||||
.withBackendName("memoryBackend")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER)));
|
||||
|
||||
qInstance.addApp(new QAppMetaData()
|
||||
.withName("myApp")
|
||||
.withSectionOfChildren(new QAppSection().withName("mySection"),
|
||||
qInstance.getTable("myTable")))
|
||||
|
||||
return (qInstance);
|
||||
}
|
||||
----
|
||||
|
||||
== Multi-method Omni-Provider
|
||||
|
||||
The next evolution of meta-data production comes by just applying some basic better-engineering
|
||||
principles, and splitting up from a single method that constructs all the things, to at least
|
||||
using unique methods to construct each thing, then calling those methods to add their results
|
||||
to the QInstance.
|
||||
|
||||
[source,java]
|
||||
.Multi-method omni- meta-data provider
|
||||
----
|
||||
public QInstance defineQInstance()
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
qInstance.setAuthentication(defineAuthenticationMetaData());
|
||||
qInstance.addBackend(defineBackendMetaData());
|
||||
qInstance.addTable(defineMyTableMetaData());
|
||||
qInstance.addApp(defineMyAppMetaData(qInstance));
|
||||
return qInstance;
|
||||
}
|
||||
|
||||
public QAuthenticationMetaData defineAuthenticationMetaData()
|
||||
{
|
||||
return new QAuthenticationMetaData()
|
||||
.withName("anonymous")
|
||||
.withType(QAuthenticationType.FULLY_ANONYMOUS);
|
||||
}
|
||||
|
||||
public QBackendMetaData defineBackendMetaData()
|
||||
{
|
||||
return new QBackendMetaData()
|
||||
.withBackendType(MemoryBackendModule.class)
|
||||
.withName("memoryBackend");
|
||||
}
|
||||
|
||||
// implementations of defineMyTableMetaData() and defineMyAppMetaData(qInstance)
|
||||
// left as an exercise for the reader
|
||||
----
|
||||
|
||||
== Multi-class Providers
|
||||
|
||||
Then the next logical evolution would be to put each of these single meta-data producing
|
||||
objects into its own class, along with calls to those classes. This gets us away from the
|
||||
"5000 line" single-class, and lets us stop using the word "omni":
|
||||
|
||||
[source,java]
|
||||
.Multi-class meta-data providers
|
||||
----
|
||||
public QInstance defineQInstance()
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
qInstance.setAuthentication(new AuthMetaDataProvider().defineAuthenticationMetaData());
|
||||
qInstance.addBackend(new BackendMetaDataProvider().defineBackendMetaData());
|
||||
qInstance.addTable(new MyTableMetaDataProvider().defineTableMetaData());
|
||||
qInstance.addApp(new MyAppMetaDataProvider().defineAppMetaData(qInstance));
|
||||
return qInstance;
|
||||
}
|
||||
|
||||
public class AuthMetaDataProvider
|
||||
{
|
||||
public QAuthenticationMetaData defineAuthenticationMetaData()
|
||||
{
|
||||
return new QAuthenticationMetaData()
|
||||
.withName("anonymous")
|
||||
.withType(QAuthenticationType.FULLY_ANONYMOUS);
|
||||
}
|
||||
}
|
||||
|
||||
public class BackendMetaDataProvider
|
||||
{
|
||||
public QBackendMetaData defineBackendMetaData()
|
||||
{
|
||||
return new QBackendMetaData()
|
||||
.withBackendType(MemoryBackendModule.class)
|
||||
.withName("memoryBackend");
|
||||
}
|
||||
}
|
||||
|
||||
// implementations of MyTableMetaDataProvider and MyAppMetaDataProvider
|
||||
// left as an exercise for the reader
|
||||
----
|
||||
|
||||
== MetaDataProducerInterface
|
||||
|
||||
As the size of your application grows, if you're doing per-object meta-data providers, you may find it
|
||||
burdensome, when adding a new object to your instance, to have to write code for it in two places -
|
||||
that is - a new class to produce that meta-data object, AND a single line of code to add that object
|
||||
to your `QInstance`. As such, a mechanism exists to let you avoid that line-of-code for adding the object
|
||||
to the `QInstance`.
|
||||
|
||||
This mechanism involves adding the `MetaDataProducerInterface` to all of your classes that produce a
|
||||
meta-data object. This interface is generic, with a type parameter that will typically be the type of
|
||||
meta-data object you are producing, such as `QTableMetaData`, `QProcessMetaData`, or `QWidgetMetaData`,
|
||||
(technically, any class which implements `TopLevelMetaData`). Implementers of the interface are then
|
||||
required to override just one method: `T produce(QInstance qInstance) throws QException;`
|
||||
|
||||
Once you have your `MetaDataProducerInterface` classes defined, then there's a one-time call needed
|
||||
to add all of the objects produced by these classes to your `QInstance` - as shown here:
|
||||
|
||||
[source,java]
|
||||
.Using MetaDataProducerInterface
|
||||
----
|
||||
public QInstance defineQInstance()
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance,
|
||||
"com.mydomain.myapplication");
|
||||
return qInstance;
|
||||
}
|
||||
|
||||
public class AuthMetaDataProducer implements MetaDataProducerInterface<QAuthenticationMetaData>
|
||||
{
|
||||
@Override
|
||||
public QAuthenticationMetaData produce(QInstance qInstance)
|
||||
{
|
||||
return new QAuthenticationMetaData()
|
||||
.withName("anonymous")
|
||||
.withType(QAuthenticationType.FULLY_ANONYMOUS);
|
||||
}
|
||||
}
|
||||
|
||||
public class BackendMetaDataProducer implements MetaDataProducerInterface<QBackendMetaData>
|
||||
{
|
||||
@Override
|
||||
public QBackendMetaData defineBackendMetaData()
|
||||
{
|
||||
return new QBackendMetaData()
|
||||
.withBackendType(MemoryBackendModule.class)
|
||||
.withName("memoryBackend");
|
||||
}
|
||||
}
|
||||
|
||||
// implementations of MyTableMetaDataProvider and MyAppMetaDataProvider
|
||||
// left as an exercise for the reader
|
||||
----
|
||||
|
||||
=== MetaDataProducerMultiOutput
|
||||
It is worth mentioning, that sometimes it might feel like a bridge that's a bit too far, to make
|
||||
every single one of your meta-data objects require its own class. Some may argue that it's best
|
||||
to do it that way - single responsibility principle, etc. But, if you're producing, say, 5 widgets
|
||||
that are all related, and it's only a handful of lines of code for each one, maybe you'd rather
|
||||
produce them all in the same class. Or maybe when you define a table, you'd like to define its
|
||||
joins and widgets at the same time.
|
||||
|
||||
This approach can be accomplished by making the type argument for your `MetaDataProducerInterface` be
|
||||
`MetaDataProducerMultiOutput` - a simple class that just wraps a list of other `MetaDataProducerOutput`
|
||||
objects.
|
||||
|
||||
[source,java]
|
||||
.Returning a MetaDataProducerMultiOutput
|
||||
----
|
||||
public class MyMultiProducer implements MetaDataProducerInterface<MetaDataProducerMultiOutput>
|
||||
{
|
||||
@Override
|
||||
public MetaDataProducerMultiOutput produce(QInstance qInstance)
|
||||
{
|
||||
MetaDataProducerMultiOutput output = new MetaDataProducerMultiOutput();
|
||||
|
||||
output.add(new QPossibleValueSource()...);
|
||||
output.add(new QJoinMetaData()...);
|
||||
output.add(new QJoinMetaData()...);
|
||||
output.add(new QWidgetMetaData()...);
|
||||
output.add(new QTableMetaData()...);
|
||||
|
||||
return (output);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
== Aside: TableMetaData with RecordEntities
|
||||
At this point, let's take a brief aside to dig deeper into the creation of a `QTableMeta` object.
|
||||
Tables, being probably the most important meta-data type in QQQ, have a lot of information that can
|
||||
be specified in their meta-data object.
|
||||
|
||||
At the same time, if you're writing any custom code in your QQQ application
|
||||
(e.g., any processes or table customizers), where you're working with records from tables, you may
|
||||
prefer being able to work with entity beans (e.g., java classes with typed getter & setter methods),
|
||||
rather than the default object type that QQQ's ORM actions return, the `QRecord`, which carries all
|
||||
of its values in a `Map` (where you don't get compile-time checks of field names or data types).
|
||||
QQQ has a mechanism for dealing with this - in the form of the `QRecordEntity` class.
|
||||
|
||||
So - if you want to build your application using entity beans (which is recommended, for the compile-time
|
||||
safety that they provide in custom code), you will be writing a `QRecordEntity` class for each of your tables,
|
||||
which will look like:
|
||||
|
||||
[source,java]
|
||||
.QRecordEntity example
|
||||
----
|
||||
public class MyTable extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "myTable";
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
@QField()
|
||||
private String name;
|
||||
|
||||
// no-arg constructor and constructor that takes a QRecord
|
||||
// getters & setters (and optional fluent setters)
|
||||
}
|
||||
----
|
||||
|
||||
The point of introducing this topic here and now is, that a `QRecordEntity` can be used to shortcut to
|
||||
defining some of the attributes in a `QTableMetaData` object. Specifically, in a `MetaDataProducer<QTableMetaData>`
|
||||
you may say:
|
||||
|
||||
[source,java]
|
||||
.QTableMetaDataProducer using a QRecordEntity
|
||||
----
|
||||
public QTableMetaData produce(QInstance qInstance) throws QExcpetion
|
||||
{
|
||||
return new QTableMetaData()
|
||||
.withName(MyTable.TABLE_NAME)
|
||||
.withFieldsFromEntity(MyTable.class)
|
||||
.withBackendName("memoryBackend");
|
||||
}
|
||||
----
|
||||
|
||||
That `withFieldsFromEntity` call is one of the biggest benefits of this technique. It allows you to avoid defining
|
||||
all of the fields in you table in two places (the entity and the table meta-data).
|
||||
|
||||
== MetaData Producing Annotations for Entities
|
||||
|
||||
If you are using `QRecordEntity` classes that correspond to your tables, then you can take advantage of some
|
||||
additional annotations on those classes, to produce more related meta-data objects associated with those tables.
|
||||
The point of this is to eliminate boilerplate, and simplify / speed up the process of getting a new table
|
||||
built and deployed in your application.
|
||||
|
||||
Furthermore, the case can be made that it is beneficial to keep the meta-data definition for a table as close
|
||||
as possible to the entity that corresponds to the table. This enables modifications to the table (e.g., adding
|
||||
a new field/column) to only require edits in one java source file, rather than necessarily requiring edits
|
||||
in two files.
|
||||
|
||||
=== @QMetaDataProducingEntity
|
||||
This is an annotation meant to be placed on a `QRecordEntity` subclass, which you would like to be
|
||||
processed by an invocation of `MetaDataProducerHelper`, to automatically produce some meta-data
|
||||
objects.
|
||||
|
||||
This annotation supports:
|
||||
|
||||
* Creating table meta-data for the corresponding record entity table. Enabled by setting `produceTableMetaData=true`.
|
||||
** One may customize the table meta data that is produced automatically by supplying a class that extends
|
||||
`MetaDataCustomizerInterface` in the annotation attribute `tableMetaDataCustomizer`.
|
||||
** In addition to (or as an alternative to) the per-table `MetaDataCustomizerInterface` that can be specified
|
||||
in `@QMetaDataProducingEntity.tableMetaDataCustomzier`, when an application calls
|
||||
`MetaDataProducerHelper.processAllMetaDataProducersInPackage`, an additional `MetaDataCustomizerInterface` can be
|
||||
given, to apply a common set of adjustments to all tales being generated by the call.
|
||||
* Making a possible-value-source out of the table. Enabled by setting `producePossibleValueSource=true`.
|
||||
* Processing child tables to create joins and childRecordList widgets
|
||||
|
||||
=== @ChildTable
|
||||
This is an annotation used as a value that goes inside a `@QMetadataProducingEntity` annotation, to define
|
||||
child-tables, e.g., for producing joins and childRecordList widgets related to the table defined in the entity class.
|
||||
|
||||
==== @ChildJoin
|
||||
This is an annotation used as a value inside a `@ChildTable` inside a `@QMetadataProducingEntity` annotation,
|
||||
to control the generation of a `QJoinMetaData`, as a `ONE_TO_MANY` type join from the table represented by
|
||||
the annotated entity, to the table referenced in the `@ChildTable` annotation.
|
||||
|
||||
==== @ChildRecordListWidget
|
||||
This is an annotation used as a value that goes inside a `@QMetadataProducingEntity` annotation, to control
|
||||
the generation of a QWidgetMetaData - for a ChildRecordList widget.
|
||||
|
||||
[source,java]
|
||||
.QRecordEntity with meta-data producing annotations and a table MetaDataCustomizer
|
||||
----
|
||||
@QMetaDataProducingEntity(
|
||||
produceTableMetaData = true,
|
||||
tableMetaDataCustomizer = MyTable.TableMetaDataCustomizer.class,
|
||||
producePossibleValueSource = true,
|
||||
childTables = {
|
||||
@ChildTable(
|
||||
childTableEntityClass = MyChildTable.class,
|
||||
childJoin = @ChildJoin(enabled = true),
|
||||
childRecordListWidget = @ChildRecordListWidget(enabled = true, label = "Children"))
|
||||
}
|
||||
)
|
||||
public class MyTable extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "myTable";
|
||||
|
||||
public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface<QTableMetaData>
|
||||
{
|
||||
@Override
|
||||
public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException
|
||||
{
|
||||
String childJoinName = QJoinMetaData.makeInferredJoinName(TABLE_NAME, MyChildTable.TABLE_NAME);
|
||||
|
||||
table
|
||||
.withUniqueKey(new UniqueKey("name"))
|
||||
.withIcon(new QIcon().withName("table_bar"))
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("name")
|
||||
|
||||
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1,
|
||||
List.of("id", "name")))
|
||||
// todo additional sections for other fields
|
||||
.withSection(new QFieldSection("children", new QIcon().withName("account_tree"), Tier.T2)
|
||||
.withWidgetName(childJoinName))
|
||||
|
||||
.withExposedJoin(new ExposedJoin()
|
||||
.withLabel("Children")
|
||||
.withJoinPath(List.of(childJoinName))
|
||||
.withJoinTable(MyChildTable.TABLE_NAME));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
@QField(isEditable = false, isPrimaryKey = true)
|
||||
private Integer id;
|
||||
|
||||
// remaining fields, constructors, getters & setters left as an exercise for the reader and/or the IDE
|
||||
}
|
||||
----
|
||||
|
||||
The class given in the example above, if processed by the `MetaDataProducerHelper`, would add the following
|
||||
meta-data objects to your `QInstance`:
|
||||
|
||||
* A `QTableMetaData` named `myTable`, with all fields annotated as `@QField` from the `QRecordEntity` class,
|
||||
and with additional attributes as set in the `TableMetaDataCustomizer` inner class.
|
||||
* A `QPossibleValueSource` named `myTable`, of type `TABLE`, with `myTable` as its backing table.
|
||||
* A `QJoinMetaData` named `myTableJoinMyChildTable`, as a `ONE_TO_MANY` type, between those two tables.
|
||||
* A `QWidgetMetaData` named `myTableJoinMyChildTable`, as a `CHILD_RECORD_LIST` type, that will show a list of
|
||||
records from `myChildTable` as a widget, when viewing a record from `myTable`.
|
||||
|
||||
== Other MetaData Producing Annotations
|
||||
|
||||
Similar to these annotations for a `RecordEntity`, a similar one exists for a `PossibleValueEnum` class,
|
||||
to automatically write the meta-data to use that enum as a possible value source in your application:
|
||||
|
||||
=== @QMetaDataProducingPossibleValueEnum
|
||||
This is an annotation to go on a `PossibleValueEnum` class, which you would like to be
|
||||
processed by MetaDataProducerHelper, to automatically produce a PossibleValueSource meta-data
|
||||
based on the enum.
|
||||
|
||||
[source,java]
|
||||
.PossibleValueEnum with meta-data producing annotation
|
||||
----
|
||||
@QMetaDataProducingPossibleValueEnum(producePossibleValueSource = true)
|
||||
public enum MyOptionsEnum implements PossibleValueEnum<Integer>
|
||||
{
|
||||
// values and methods left as exercise for reader
|
||||
}
|
||||
----
|
||||
The enum given in the example above, if processed by the `MetaDataProducerHelper`, would add the following
|
||||
meta-data object to your `QInstance`:
|
||||
|
||||
* A `QPossibleValueSource` named `MyOptionsEnum`, of type `ENUM`, with `MyOptionsEnum` as its backing enum.
|
@ -38,6 +38,13 @@ See {link-permissionRules} for details.
|
||||
*** 1) by a single call to `.withStepList(List<QStepMetaData>)`, which internally adds each step into the `steps` map.
|
||||
*** 2) by multiple calls to `.addStep(QStepMetaData)`, which adds a step to both the `stepList` and `steps` map.
|
||||
** If a process also needs optional steps (for a <<_custom_process_flow>>), they should be added by a call to `.addOptionalStep(QStepMetaData)`, which only places them in the `steps` map.
|
||||
* `stepFlow` - *enum, default LINEAR* - specifies the the flow-control logic between steps. Possible values are:
|
||||
** `LINEAR` - steps are executed in-order, through the `stepList`.
|
||||
A backend step _can_ customize the `nextStepName` or re-order the `stepList`, if needed.
|
||||
In a frontend step, a user may be given the option to go _back_ to a previous step as well.
|
||||
** `STATE_MACHINE` - steps are executed as a Fine State Machine, starting with the first step in `stepList`,
|
||||
but then proceeding based on the `nextStepName` specified by the previous step.
|
||||
Thus allowing much more flexible flows.
|
||||
* `schedule` - *<<QScheduleMetaData>>* - set up the process to run automatically on the specified schedule.
|
||||
See below for details.
|
||||
* `minInputRecords` - *Integer* - #not used...#
|
||||
@ -67,6 +74,11 @@ For processes with a user-interface, they must define one or more "screens" in t
|
||||
* `formFields` - *List of String* - list of field names used by the screen as form-inputs.
|
||||
* `viewFields` - *List of String* - list of field names used by the screen as visible outputs.
|
||||
* `recordListFields` - *List of String* - list of field names used by the screen in a record listing.
|
||||
* `format` - *Optional String* - directive for a frontend to use specialized formatting for the display of the process.
|
||||
** Consult frontend documentation for supported values and their meanings.
|
||||
* `backStepName` - *Optional String* - For processes using `LINEAR` flow, if this value is given,
|
||||
then the frontend should offer a control that the user can take (e.g., a button) to move back to an
|
||||
earlier step in the process.
|
||||
|
||||
==== QFrontendComponentMetaData
|
||||
|
||||
@ -90,10 +102,13 @@ Expects a process value named `html`.
|
||||
Expects process values named `downloadFileName` and `serverFilePath`.
|
||||
** `GOOGLE_DRIVE_SELECT_FOLDER` - Special form that presents a UI from Google Drive, where the user can select a folder (e.g., as a target for uploading files in a subsequent backend step).
|
||||
** `BULK_EDIT_FORM` - For use by the standard QQQ Bulk Edit process.
|
||||
** `BULK_LOAD_FILE_MAPPING_FORM`, `BULK_LOAD_VALUE_MAPPING_FORM`, or `BULK_LOAD_PROFILE_FORM` - For use by the standard QQQ Bulk Load process.
|
||||
** `VALIDATION_REVIEW_SCREEN` - For use by the QQQ Streamed ETL With Frontend process family of processes.
|
||||
Displays a component prompting the user to run full validation or to skip it, or, if full validation has been ran, then showing the results of that validation.
|
||||
** `PROCESS_SUMMARY_RESULTS` - For use by the QQQ Streamed ETL With Frontend process family of processes.
|
||||
Displays the summary results of running the process.
|
||||
** `WIDGET` - Render a QQQ Widget.
|
||||
Requires that `widgetName` be given as a value for the component.
|
||||
** `RECORD_LIST` - _Deprecated.
|
||||
Showed a grid with a list of records as populated by the process._
|
||||
* `values` - *Map of String → Serializable* - Key=value pairs, with different expectations based on the component's `type`.
|
||||
@ -116,6 +131,27 @@ It can be used, however, for example, to cause a `defaultValue` to be applied to
|
||||
It can also be used to cause the process to throw an error, if a field is marked as `isRequired`, but a value is not present.
|
||||
** `recordListMetaData` - *RecordListMetaData object* - _Not used at this time._
|
||||
|
||||
==== QStateMachineStep
|
||||
|
||||
Processes that use `flow = STATE_MACHINE` should use process steps of type `QStateMachineStep`.
|
||||
|
||||
A common pattern seen in state-machine processes, is that they will present a frontend-step to a user,
|
||||
then always run a given backend-step in response to that screen which the user submitted.
|
||||
Inside that backend-step, custom application logic will determine the next state to go to,
|
||||
which is typically another frontend-step (which would then submit data to its corresponding backend-step,
|
||||
and continue the FSM).
|
||||
|
||||
To help facilitate this pattern, factory methods exist on `QStateMachineStep`,
|
||||
for constructing the commonly-expected types of state-machine steps:
|
||||
|
||||
* `frontendThenBackend(name, frontendStep, backendStep)` - for the frontend-then-backend pattern described above.
|
||||
* `backendOnly(name, backendStep)` - for a state that only has a backend step.
|
||||
This might be useful as a “reset” step, to run before restarting a state-loop.
|
||||
* `frontendOnly(name, frontendStep)` - for a state that only has a frontend step,
|
||||
which would always be followed by another state, which must be specified as the `defaultNextStepName`
|
||||
on the `QStateMachineStep`.
|
||||
|
||||
|
||||
==== BasepullConfiguration
|
||||
|
||||
A "Basepull" process is a common pattern where an application needs to perform some action on all new (or updated) records from a particular data source.
|
||||
@ -218,12 +254,10 @@ But for some cases, doing page-level transactions can reduce long-transactions a
|
||||
* `withSchedule(QScheduleMetaData schedule)` - Add a <<QScheduleMetaData>> to the process.
|
||||
|
||||
[#_custom_process_flow]
|
||||
==== Custom Process Flow
|
||||
As referenced in the definition of the <<_QProcessMetaData_Properties,QProcessMetaData Properties>>, by default, a process
|
||||
will execute each of its steps in-order, as defined in the `stepList` property.
|
||||
However, a Backend Step can customize this flow #todo - write more clearly here...
|
||||
|
||||
There are generally 2 method to call (in a `BackendStep`) to do a dynamic flow:
|
||||
==== How to customize a Linear process flow
|
||||
As referenced in the definition of the <<_QProcessMetaData_Properties,QProcessMetaData Properties>>, by default,
|
||||
(with `flow = LINEAR`) a process will execute each of its steps in-order, as defined in the `stepList` property.
|
||||
However, a Backend Step can customize this flow as follows:
|
||||
|
||||
* `RunBackendStepOutput.setOverrideLastStepName(String stepName)`
|
||||
** QQQ's `RunProcessAction` keeps track of which step it "last" ran, e.g., to tell it which one to run next.
|
||||
@ -239,7 +273,7 @@ does need to be found in the new `stepNameList` - otherwise, the framework will
|
||||
for figuring out where to go next.
|
||||
|
||||
[source,java]
|
||||
.Example of a defining process that can use a flexible flow:
|
||||
.Example of a defining process that can use a customized linear flow:
|
||||
----
|
||||
// for a case like this, it would be recommended to define all step names in constants:
|
||||
public final static String STEP_START = "start";
|
||||
@ -324,4 +358,21 @@ public static class StartStep implements BackendStep
|
||||
}
|
||||
----
|
||||
|
||||
[#_process_back]
|
||||
==== How to allow a process to go back
|
||||
|
||||
The simplest option to allow a process to present a "Back" button to users,
|
||||
thus allowing them to move backward through a process
|
||||
(e.g., from a review screen back to an earlier input screen), is to set the property `backStepName`
|
||||
on a `QFrontendStepMetaData`.
|
||||
|
||||
If the step that is executed after the user hits "Back" is a backend step, then within that
|
||||
step, `runBackendStepInput.getIsStepBack()` will return `true` (but ONLY within that first step after
|
||||
the user hits "Back"). It may be necessary within individual processes to be aware that the user
|
||||
has chosen to go back, to reset certain values in the process's state.
|
||||
|
||||
Alternatively, if a frontend step's "Back" behavior needs to be dynamic (e.g., sometimes not available,
|
||||
or sometimes targeting different steps in the process), then in a backend step that runs before the
|
||||
frontend step, a call to `runBackendStepOutput.getProcessState().setBackStepName()` can be made,
|
||||
to customize the value which would otherwise come from the `QFrontendStepMetaData`.
|
||||
|
||||
|
@ -29,11 +29,21 @@ service.routes(qJavalinImplementation.getRoutes());
|
||||
service.start();
|
||||
----
|
||||
|
||||
*QBackendMetaData Setup Methods:*
|
||||
*QInstance Setup:*
|
||||
|
||||
These are the methods that one is most likely to use when setting up (defining) a `QInstance` object:
|
||||
|
||||
* asdf
|
||||
* `add(TopLevelMetaDataInterface metaData)` - Generic method that takes most of the meta-data subtypes that can be added
|
||||
to an instance, such as `QBackendMetaData`, `QTableMetaData`, `QProcessMetaData`, etc.
|
||||
There are also type-specific methods (e.g., `addTable`, `addProcess`, etc), which one can call instead - this would just
|
||||
be a matter of personal preference.
|
||||
|
||||
*QBackendMetaData Usage Methods:*
|
||||
*QInstance Usage:*
|
||||
|
||||
Generally you will set up a `QInstance` in your application's startup flow, and then place it in the server (e.g., javalin).
|
||||
But, if, during application-code runtime, you need access to any of the meta-data in the instance, you access it
|
||||
via the `QContext` object's static `getInstance()` method. This can be useful, for example, to get a list of the defined
|
||||
tables in the application, or fields in a table, or details about a field, etc.
|
||||
|
||||
It is generally considered risky and/or not a good idea at all to modify the `QInstance` after it has been validated and
|
||||
a server is running. Future versions of QQQ may in fact restrict modifications to the instance after validation.
|
||||
|
109
docs/misc/Javalin.adoc
Normal file
109
docs/misc/Javalin.adoc
Normal file
@ -0,0 +1,109 @@
|
||||
== QQQ Middleware: Javalin web server
|
||||
include::../variables.adoc[]
|
||||
|
||||
QQQ provides a standard implementation of a middleware layer - that is - code that exists between the
|
||||
QQQ backend and user interface. This implementation is a web server built using the https://javalin.io/[Javalin framework],
|
||||
packaged and deployed in the `qqq-middleware-javalin` maven module
|
||||
|
||||
The de facto way to create a QQQ application server is to write a class which uses an instance of one of the
|
||||
subclasses of `QApplicationJavalinServer`.
|
||||
|
||||
For example, if your application metadata is defined in a directory of yaml files, your server class could be implemented as:
|
||||
|
||||
[source,java]
|
||||
.ConfigFileBasedQQQApplication usage example
|
||||
----
|
||||
public static void main(String[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
String path = "src/main/resources/metadata";
|
||||
ConfigFilesBasedQQQApplication application = new ConfigFilesBasedQQQApplication(path);
|
||||
QApplicationJavalinServer javalinServer = new QApplicationJavalinServer(application);
|
||||
javalinServer.start();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Failed to start javalin server. See stack trace for details.", e);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
A similar class exists if your metadata is produced by a package of Java MetaDataProducer objects: `MetaDataProducerBasedQQQApplication`.
|
||||
|
||||
=== QApplicationJavalinServer
|
||||
This class provides the bridge between your QQQ Application (e.g., your metadata) and the QQQ Middleware layer
|
||||
served by a Javalin web server. It has several properties to control behaviors:
|
||||
|
||||
* `Integer port` - (default `8000`) - port to use for serving HTTP.
|
||||
* `boolean serveFrontendMaterialDashboard` - (default `true`) whether to serve the javascript frontend provided
|
||||
in the maven artifact `qqq-frontend-material-dashboard`.
|
||||
* `boolean serveLegacyUnversionedMiddlewareAPI` - (default `true`) whether to serve a version the original implementation
|
||||
of the QQQ middleware, which current version of `qqq-frontend-material-dashboard` are compatible with.
|
||||
* `List<AbstractMiddlewareVersion> middlewareVersionList` - (default contains `MiddlewareVersionV1`) - list of
|
||||
newer, formally versioned implementations of the QQQ middleware interface to be served.
|
||||
* `Consumer<Javalin> javalinConfigurationCustomizer` - (default `null`) - optional hook to customize the
|
||||
javalin service object before it is started.
|
||||
* `List<QJavalinRouteProviderInterface> additionalRouteProviders` - (default `null`) - list of fully custom
|
||||
implementations of `QJavalinRouteProviderInterface`, to add additional endpoints to the javalin server.
|
||||
** _Note, you may first want to consider using JavalinRouteProviderMetaData instead - see below._
|
||||
* `QJavalinMetaData javalinMetaData` - (default `null`) - optional alternative place to define `JavalinMetaData` (vs.
|
||||
defining it in the `QInstance`). _Note that if it is set in both places, the one in the QApplicationJavalinServer
|
||||
is used._
|
||||
|
||||
=== JavalinMetaData
|
||||
Certain behaviors of a QQQ Javalin server are configured in a declarative manner by adding a `QJavalinMetaData`
|
||||
object to the `supplementalMetaData` in your `QInstance` (or, as mentioned above, by setting it directly on the
|
||||
`QApplicationJavalinServer`):
|
||||
|
||||
* `List<JavalinRouteProviderMetaData> routeProviders` - (default `null`) optional list of custom route providers to
|
||||
add to the Javalin server. See below for details.
|
||||
* `String uploadedFileArchiveTableName` - (default `null`) - reference to a QQQ Table in your application instance,
|
||||
needed to support the Bulk Load process, as well as any other processes which need to accept an uploaded file
|
||||
as input.
|
||||
* `boolean loggerDisabled` - (default `false`)
|
||||
* `Function<QJavalinAccessLogger.LogEntry, Boolean> logFilter` - (default `null`)
|
||||
* `boolean queryWithoutLimitAllowed` - (default `false`)
|
||||
* `Integer queryWithoutLimitDefault` - (default `1000`)
|
||||
* `Level queryWithoutLimitLogLevel` - (default `INFO`)
|
||||
|
||||
==== JavalinRouteProviderMetaData
|
||||
This type of metadata allows you to add additional http route providers to your Javalin instance, e.g., for
|
||||
serving static files or for running custom code from your application (in the form of QQQ Processes) to respond
|
||||
to HTTP requests.
|
||||
|
||||
* `String hostedPath` - (required)
|
||||
* `String fileSystemPath` - (required for a static router)
|
||||
* `String processName` - required for a dynamic, process-based router. Must be a process name within the QQQ Instance.
|
||||
See below for additional details
|
||||
* `List<String> methods` - required list of HTTP methods (verbs) that are served by the route provider
|
||||
* `QCodeReference routeAuthenticator - Optional reference to a class that implements `RouteAuthenticatorInterface`,
|
||||
to provide security authentication to all requests handled by the route provider.
|
||||
** A default implementation is provided as `SimpleRouteAuthenticator`, which requires that a user session be present
|
||||
to access paths served by the route provider.
|
||||
|
||||
===== Process-based route provider processes
|
||||
If you define a `JavalinRouteProviderMetaData` with a `processName` (e.g., to serve dynamic HTTP responses from your javalin
|
||||
server), the process that you implement will be called to respond to any HTTP requests received by the javalin
|
||||
server which match the `hostedPath` and `methods` that are specified in the metadata.
|
||||
|
||||
The QQQ javalin server will marshal request data from the javalin context into the process's payload, conforming to
|
||||
the shape of the `ProcessBasedRouterPayload` class. Similarly, the http response will be built by taking values from
|
||||
the process's output/state conforming to the fields in that class. As such, it is recommended to use a
|
||||
`ProcessBasedRouterPayload` instance, as show in this example:
|
||||
|
||||
[source,java]
|
||||
.Process-based router usage example (including ProcessBasedRouterPayload)
|
||||
----
|
||||
public class MyDynamicSiteProcessStep implements BackendStep
|
||||
{
|
||||
@Override
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
|
||||
{
|
||||
ProcessBasedRouterPayload payload = input.getProcessPayload(ProcessBasedRouterPayload.class);
|
||||
String path = payload.getPath();
|
||||
payload.setResponseString("You requested: " + path);
|
||||
output.setProcessPayload(payload);
|
||||
}
|
||||
}
|
||||
----
|
8
pom.xml
8
pom.xml
@ -34,8 +34,10 @@
|
||||
<module>qqq-backend-module-api</module>
|
||||
<module>qqq-backend-module-filesystem</module>
|
||||
<module>qqq-backend-module-rdbms</module>
|
||||
<module>qqq-backend-module-sqlite</module>
|
||||
<module>qqq-backend-module-mongodb</module>
|
||||
<module>qqq-language-support-javascript</module>
|
||||
<module>qqq-openapi</module>
|
||||
<module>qqq-middleware-picocli</module>
|
||||
<module>qqq-middleware-javalin</module>
|
||||
<module>qqq-middleware-lambda</module>
|
||||
@ -46,12 +48,11 @@
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<revision>0.21.0-SNAPSHOT</revision>
|
||||
<revision>0.25.0</revision>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
<maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
|
||||
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
|
||||
<coverage.haltOnFailure>true</coverage.haltOnFailure>
|
||||
@ -168,6 +169,7 @@
|
||||
<violationSeverity>warning</violationSeverity>
|
||||
<excludes>**/target/generated-sources/*.*</excludes>
|
||||
<!-- <linkXRef>false</linkXRef> -->
|
||||
<includeTestSourceDirectory>true</includeTestSourceDirectory>
|
||||
</configuration>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
|
@ -65,7 +65,11 @@
|
||||
<artifactId>aws-java-sdk-secretsmanager</artifactId>
|
||||
<version>1.12.385</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
<artifactId>icu4j</artifactId>
|
||||
<version>77.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
@ -100,7 +104,12 @@
|
||||
<dependency>
|
||||
<groupId>org.dhatim</groupId>
|
||||
<artifactId>fastexcel</artifactId>
|
||||
<version>0.12.15</version>
|
||||
<version>0.18.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dhatim</groupId>
|
||||
<artifactId>fastexcel-reader</artifactId>
|
||||
<version>0.18.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
@ -112,10 +121,24 @@
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.2.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- adding to help FastExcel -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.16.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.nimbusds</groupId>
|
||||
<artifactId>oauth2-oidc-sdk</artifactId>
|
||||
<version>11.23.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>auth0</artifactId>
|
||||
<version>2.1.0</version>
|
||||
<version>2.18.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
@ -125,12 +148,12 @@
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>jwks-rsa</artifactId>
|
||||
<version>0.22.0</version>
|
||||
<version>0.22.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.cdimascio</groupId>
|
||||
<artifactId>java-dotenv</artifactId>
|
||||
<version>5.2.2</version>
|
||||
<artifactId>dotenv-java</artifactId>
|
||||
<version>3.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
@ -138,16 +161,21 @@
|
||||
<version>2.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- the next 2 deps are for html to pdf - per https://www.baeldung.com/java-html-to-pdf -->
|
||||
<!-- the next 3 deps are for html to pdf -->
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.15.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xhtmlrenderer</groupId>
|
||||
<artifactId>flying-saucer-pdf-openpdf</artifactId>
|
||||
<version>9.1.22</version>
|
||||
<groupId>com.openhtmltopdf</groupId>
|
||||
<artifactId>openhtmltopdf-core</artifactId>
|
||||
<version>1.0.10</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.openhtmltopdf</groupId>
|
||||
<artifactId>openhtmltopdf-pdfbox</artifactId>
|
||||
<version>1.0.10</version>
|
||||
</dependency>
|
||||
|
||||
<!-- the next 3 deps are being added for google drive support -->
|
||||
|
@ -37,7 +37,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
**
|
||||
** Note: One would imagine that this class shouldn't ever implement Serializable...
|
||||
*******************************************************************************/
|
||||
public class QBackendTransaction
|
||||
public class QBackendTransaction implements AutoCloseable
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
|
@ -186,7 +186,7 @@ public class AsyncRecordPipeLoop
|
||||
|
||||
if(recordCount > 0)
|
||||
{
|
||||
LOG.info("End of job summary", logPair("recordCount", recordCount), logPair("jobName", jobName), logPair("millis", endTime - jobStartTime), logPair("recordsPerSecond", 1000d * (recordCount / (.001d + (endTime - jobStartTime)))));
|
||||
LOG.debug("End of job summary", logPair("recordCount", recordCount), logPair("jobName", jobName), logPair("millis", endTime - jobStartTime), logPair("recordsPerSecond", 1000d * (recordCount / (.001d + (endTime - jobStartTime)))));
|
||||
}
|
||||
|
||||
return (recordCount);
|
||||
|
@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
@ -173,26 +174,53 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
Map<String, Serializable> securityKeyValues = new HashMap<>();
|
||||
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
|
||||
{
|
||||
Serializable keyValue = record == null ? null : record.getValue(recordSecurityLock.getFieldName());
|
||||
|
||||
if(keyValue == null && oldRecord.isPresent())
|
||||
{
|
||||
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()));
|
||||
keyValue = oldRecord.get().getValue(recordSecurityLock.getFieldName());
|
||||
}
|
||||
|
||||
if(keyValue == null)
|
||||
{
|
||||
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()), logPair("oldRecordIsPresent", oldRecord.isPresent()));
|
||||
}
|
||||
|
||||
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), keyValue);
|
||||
getRecordSecurityKeyValues(table, record, oldRecord, recordSecurityLock, securityKeyValues);
|
||||
}
|
||||
return securityKeyValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** recursive implementation of getRecordSecurityKeyValues, for dealing with
|
||||
** multi-locks
|
||||
***************************************************************************/
|
||||
private static void getRecordSecurityKeyValues(QTableMetaData table, QRecord record, Optional<QRecord> oldRecord, RecordSecurityLock recordSecurityLock, Map<String, Serializable> securityKeyValues)
|
||||
{
|
||||
//////////////////////////////////////////////////////
|
||||
// special case with recursive call for multi-locks //
|
||||
//////////////////////////////////////////////////////
|
||||
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
|
||||
{
|
||||
for(RecordSecurityLock subLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks())))
|
||||
{
|
||||
getRecordSecurityKeyValues(table, record, oldRecord, subLock, securityKeyValues);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////
|
||||
// by default, deal with non-multi locks //
|
||||
///////////////////////////////////////////
|
||||
Serializable keyValue = record == null ? null : record.getValue(recordSecurityLock.getFieldName());
|
||||
|
||||
if(keyValue == null && oldRecord.isPresent())
|
||||
{
|
||||
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()));
|
||||
keyValue = oldRecord.get().getValue(recordSecurityLock.getFieldName());
|
||||
}
|
||||
|
||||
if(keyValue == null)
|
||||
{
|
||||
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()), logPair("oldRecordIsPresent", oldRecord.isPresent()));
|
||||
}
|
||||
|
||||
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), keyValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -218,15 +246,16 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
throw (new QException("Requested audit for an unrecognized table name: " + auditSingleInput.getAuditTableName()));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// validate security keys on the table are given //
|
||||
///////////////////////////////////////////////////
|
||||
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
|
||||
///////////////////////////////////////////////////////
|
||||
// validate security keys on the table are given //
|
||||
// originally, this case threw... //
|
||||
// but i think it's better to record the audit, just //
|
||||
// missing its security key value, then to fail... //
|
||||
// but, maybe should be configurable, etc... //
|
||||
///////////////////////////////////////////////////////
|
||||
if(!validateSecurityKeys(auditSingleInput, table))
|
||||
{
|
||||
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
|
||||
{
|
||||
throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + auditSingleInput.getAuditTableName()));
|
||||
}
|
||||
LOG.debug("Missing securityKeyValue in audit request", logPair("table", auditSingleInput.getAuditTableName()), logPair("auditMessage", auditSingleInput.getMessage()), logPair("recordId", auditSingleInput.getRecordId()));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
@ -272,7 +301,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
List<QRecord> auditDetailRecords = new ArrayList<>();
|
||||
for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList()))
|
||||
{
|
||||
Integer auditId = insertOutput.getRecords().get(i++).getValueInteger("id");
|
||||
Long auditId = insertOutput.getRecords().get(i++).getValueLong("id");
|
||||
if(auditId == null)
|
||||
{
|
||||
LOG.warn("Missing an id for inserted audit - so won't be able to store its child details...");
|
||||
@ -304,6 +333,70 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static boolean validateSecurityKeys(AuditSingleInput auditSingleInput, QTableMetaData table)
|
||||
{
|
||||
boolean allAreValid = true;
|
||||
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
|
||||
{
|
||||
boolean lockIsValid = validateSecurityKeysForLock(auditSingleInput, recordSecurityLock);
|
||||
if(!lockIsValid)
|
||||
{
|
||||
allAreValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
return (allAreValid);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static boolean validateSecurityKeysForLock(AuditSingleInput auditSingleInput, RecordSecurityLock recordSecurityLock)
|
||||
{
|
||||
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
|
||||
{
|
||||
boolean allSubLocksAreValid = true;
|
||||
boolean anySubLocksAreValid = false;
|
||||
for(RecordSecurityLock lock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks())))
|
||||
{
|
||||
boolean subLockIsValid = validateSecurityKeysForLock(auditSingleInput, lock);
|
||||
if(subLockIsValid)
|
||||
{
|
||||
anySubLocksAreValid = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
allSubLocksAreValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(multiRecordSecurityLock.getOperator().equals(MultiRecordSecurityLock.BooleanOperator.OR))
|
||||
{
|
||||
return (anySubLocksAreValid);
|
||||
}
|
||||
else if(multiRecordSecurityLock.getOperator().equals(MultiRecordSecurityLock.BooleanOperator.AND))
|
||||
{
|
||||
return (allSubLocksAreValid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -344,6 +344,9 @@ public class RecordAutomationStatusUpdater
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private record Key(QTableMetaData table, TriggerEvent triggerEvent) {}
|
||||
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ public class RunRecordScriptAutomationHandler extends RecordAutomationHandler
|
||||
}
|
||||
|
||||
QRecord scriptRevision = queryOutput.getRecords().get(0);
|
||||
LOG.info("Running script against records", logPair("scriptRevisionId", scriptRevision.getValue("id")), logPair("scriptId", scriptRevision.getValue("scriptIdd")));
|
||||
LOG.debug("Running script against records", logPair("scriptRevisionId", scriptRevision.getValue("id")), logPair("scriptId", scriptRevision.getValue("scriptIdd")));
|
||||
|
||||
RunAdHocRecordScriptInput input = new RunAdHocRecordScriptInput();
|
||||
input.setCodeReference(new AdHocScriptCodeReference().withScriptRevisionRecord(scriptRevision));
|
||||
|
@ -649,7 +649,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
input.setRecordList(records);
|
||||
input.setAction(action);
|
||||
|
||||
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getRecordAutomationHandler(action);
|
||||
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getAdHoc(RecordAutomationHandler.class, action.getCodeReference());
|
||||
recordAutomationHandler.execute(input);
|
||||
}
|
||||
}
|
||||
|
@ -55,10 +55,10 @@ public abstract class AbstractPreInsertCustomizer implements TableCustomizerInte
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// allow the customizer to specify when it should be executed as part of the //
|
||||
// insert action. default (per method in this class) is AFTER_ALL_VALIDATIONS //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
/***************************************************************************
|
||||
** allow the customizer to specify when it should be executed as part of the
|
||||
** insert action. default (per method in this class) is AFTER_ALL_VALIDATIONS
|
||||
***************************************************************************/
|
||||
public enum WhenToRun
|
||||
{
|
||||
BEFORE_ALL_VALIDATIONS,
|
||||
|
@ -28,6 +28,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
@ -49,6 +50,9 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
*******************************************************************************/
|
||||
public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInsertCustomizer
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public enum RelationshipType
|
||||
{
|
||||
PARENT_POINTS_AT_CHILD,
|
||||
@ -97,7 +101,7 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
|
||||
List<QRecord> rs = records;
|
||||
List<QRecord> childrenToInsert = new ArrayList<>();
|
||||
QTableMetaData table = getInsertInput().getTable();
|
||||
QTableMetaData childTable = getInsertInput().getInstance().getTable(getChildTableName());
|
||||
QTableMetaData childTable = QContext.getQInstance().getTable(getChildTableName());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// iterate over the inserted records, building a list child records to insert //
|
||||
|
@ -24,17 +24,11 @@ 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;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
|
||||
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.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;
|
||||
@ -43,9 +37,7 @@ 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.
|
||||
** That memoization causes 1,000,000 such calls to go from ~500ms to ~100ms.
|
||||
*******************************************************************************/
|
||||
public class QCodeLoader
|
||||
{
|
||||
@ -70,84 +62,6 @@ public class QCodeLoader
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T, R> Function<T, R> getFunction(QCodeReference codeReference)
|
||||
{
|
||||
if(codeReference == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
throw (new IllegalArgumentException("Only JAVA customizers are supported at this time."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Class<?> customizerClass = Class.forName(codeReference.getName());
|
||||
return ((Function<T, R>) customizerClass.getConstructor().newInstance());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
|
||||
// as we'll want to validate all functions in the instance validator at startup time (and IT will throw //
|
||||
// if it finds an invalid code reference //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends BackendStep> T getBackendStep(Class<T> expectedType, QCodeReference codeReference)
|
||||
{
|
||||
if(codeReference == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
throw (new IllegalArgumentException("Only JAVA BackendSteps are supported at this time."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Class<?> customizerClass = Class.forName(codeReference.getName());
|
||||
return ((T) customizerClass.getConstructor().newInstance());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
|
||||
// as we'll want to validate all functions in the instance validator at startup time (and IT will throw //
|
||||
// if it finds an invalid code reference //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -177,7 +91,17 @@ public class QCodeLoader
|
||||
|
||||
if(constructor.isPresent())
|
||||
{
|
||||
return ((T) constructor.get().newInstance());
|
||||
T t = (T) constructor.get().newInstance();
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// if the object is initializable, then, well, initialize it! //
|
||||
////////////////////////////////////////////////////////////////
|
||||
if(t instanceof InitializableViaCodeReference initializableViaCodeReference)
|
||||
{
|
||||
initializableViaCodeReference.initialize(codeReference);
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -187,7 +111,7 @@ public class QCodeLoader
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
|
||||
LOG.error("Error initializing codeReference", e, logPair("codeReference", codeReference));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
|
||||
@ -198,67 +122,4 @@ public class QCodeLoader
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static RecordAutomationHandler getRecordAutomationHandler(TableAutomationAction action) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
QCodeReference codeReference = action.getCodeReference();
|
||||
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
throw (new IllegalArgumentException("Only JAVA customizers are supported at this time."));
|
||||
}
|
||||
|
||||
Class<?> codeClass = Class.forName(codeReference.getName());
|
||||
Object codeObject = codeClass.getConstructor().newInstance();
|
||||
if(!(codeObject instanceof RecordAutomationHandler recordAutomationHandler))
|
||||
{
|
||||
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of RecordAutomationHandler"));
|
||||
}
|
||||
return (recordAutomationHandler);
|
||||
}
|
||||
catch(QException qe)
|
||||
{
|
||||
throw (qe);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QException("Error getting record automation handler for action [" + action.getName() + "]", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QCustomPossibleValueProvider getCustomPossibleValueProvider(QPossibleValueSource possibleValueSource) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
Class<?> codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName());
|
||||
Object codeObject = codeClass.getConstructor().newInstance();
|
||||
if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider))
|
||||
{
|
||||
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider"));
|
||||
}
|
||||
return (customPossibleValueProvider);
|
||||
}
|
||||
catch(QException qe)
|
||||
{
|
||||
throw (qe);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QException("Error getting custom possible value provider for PVS [" + possibleValueSource.getName() + "]", e));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,10 +24,13 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
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.actions.tables.update.UpdateInput;
|
||||
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.tables.QTableMetaData;
|
||||
@ -143,4 +146,33 @@ public interface RecordCustomizerUtilityInterface
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default Map<Serializable, QRecord> getOldRecordMap(List<QRecord> oldRecordList, UpdateInput updateInput)
|
||||
{
|
||||
Map<Serializable, QRecord> oldRecordMap = new HashMap<>();
|
||||
for(QRecord qRecord : oldRecordList)
|
||||
{
|
||||
oldRecordMap.put(qRecord.getValue(updateInput.getTable().getPrimaryKeyField()), qRecord);
|
||||
}
|
||||
|
||||
return (oldRecordMap);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static <T extends Serializable> T getValueFromRecordOrOldRecord(String fieldName, QRecord record, Serializable primaryKey, Optional<Map<Serializable, QRecord>> oldRecordMap)
|
||||
{
|
||||
T value = (T) record.getValue(fieldName);
|
||||
if(value == null && primaryKey != null && oldRecordMap.isPresent() && oldRecordMap.get().containsKey(primaryKey))
|
||||
{
|
||||
value = (T) oldRecordMap.get().get(primaryKey).getValue(fieldName);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,15 +22,19 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
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.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
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 com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -47,7 +51,6 @@ public interface TableCustomizerInterface
|
||||
{
|
||||
QLogger LOG = QLogger.getLogger(TableCustomizerInterface.class);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** custom actions to run after a query (or get!) takes place.
|
||||
**
|
||||
@ -77,8 +80,15 @@ public interface TableCustomizerInterface
|
||||
*******************************************************************************/
|
||||
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);
|
||||
try
|
||||
{
|
||||
return (preInsertOrUpdate(insertInput, records, isPreview, Optional.empty()));
|
||||
}
|
||||
catch(NotImplementedHereException e)
|
||||
{
|
||||
LOG.info("A default implementation of preInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
|
||||
return (records);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -104,8 +114,15 @@ public interface TableCustomizerInterface
|
||||
*******************************************************************************/
|
||||
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);
|
||||
try
|
||||
{
|
||||
return (postInsertOrUpdate(insertInput, records, Optional.empty()));
|
||||
}
|
||||
catch(NotImplementedHereException e)
|
||||
{
|
||||
LOG.info("A default implementation of postInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
|
||||
return (records);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -130,8 +147,15 @@ public interface TableCustomizerInterface
|
||||
*******************************************************************************/
|
||||
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);
|
||||
try
|
||||
{
|
||||
return (preInsertOrUpdate(updateInput, records, isPreview, oldRecordList));
|
||||
}
|
||||
catch(NotImplementedHereException e)
|
||||
{
|
||||
LOG.info("A default implementation of preUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
|
||||
return (records);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -151,8 +175,15 @@ public interface TableCustomizerInterface
|
||||
*******************************************************************************/
|
||||
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);
|
||||
try
|
||||
{
|
||||
return (postInsertOrUpdate(updateInput, records, oldRecordList));
|
||||
}
|
||||
catch(NotImplementedHereException e)
|
||||
{
|
||||
LOG.info("A default implementation of postUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
|
||||
return (records);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -199,4 +230,59 @@ public interface TableCustomizerInterface
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Optional method to override in a customizer that does the same thing for
|
||||
** both preInsert & preUpdate.
|
||||
***************************************************************************/
|
||||
default List<QRecord> preInsertOrUpdate(AbstractActionInput input, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
throw NotImplementedHereException.instance;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Optional method to override in a customizer that does the same thing for
|
||||
** both postInsert & postUpdate.
|
||||
***************************************************************************/
|
||||
default List<QRecord> postInsertOrUpdate(AbstractActionInput input, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
throw NotImplementedHereException.instance;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
default Optional<Map<Serializable, QRecord>> oldRecordListToMap(String primaryKeyField, Optional<List<QRecord>> oldRecordList)
|
||||
{
|
||||
if(oldRecordList.isPresent())
|
||||
{
|
||||
return (Optional.of(CollectionUtils.listToMap(oldRecordList.get(), r -> r.getValue(primaryKeyField))));
|
||||
}
|
||||
else
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
class NotImplementedHereException extends QException
|
||||
{
|
||||
private static NotImplementedHereException instance = new NotImplementedHereException();
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private NotImplementedHereException()
|
||||
{
|
||||
super("Not implemented here");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -290,7 +290,18 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Deprecated(since = "call one that doesn't take input param")
|
||||
public static String linkRecordEdit(AbstractActionInput input, String tableName, Serializable recordId) throws QException
|
||||
{
|
||||
return linkRecordEdit(tableName, recordId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String linkRecordEdit(String tableName, Serializable recordId) throws QException
|
||||
{
|
||||
String tablePath = QContext.getQInstance().getTablePath(tableName);
|
||||
return (tablePath + "/" + recordId + "/edit");
|
||||
@ -317,7 +328,17 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Deprecated(since = "call one that doesn't take input param")
|
||||
public static String linkProcessForFilter(AbstractActionInput input, String processName, QQueryFilter filter) throws QException
|
||||
{
|
||||
return linkProcessForFilter(processName, filter);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String linkProcessForFilter(String processName, QQueryFilter filter) throws QException
|
||||
{
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
if(process == null)
|
||||
@ -337,10 +358,21 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Deprecated(since = "call one that doesn't take input param")
|
||||
public static String linkProcessForRecord(AbstractActionInput input, String processName, Serializable recordId) throws QException
|
||||
{
|
||||
return linkProcessForRecord(processName, recordId);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String linkProcessForRecord(String processName, Serializable recordId) throws QException
|
||||
{
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
String tableName = process.getTableName();
|
||||
|
@ -31,6 +31,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
@ -102,7 +103,7 @@ public abstract class AbstractWidgetRenderer
|
||||
String possibleValueSourceName = dropdownData.getPossibleValueSourceName();
|
||||
if(possibleValueSourceName != null)
|
||||
{
|
||||
QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName);
|
||||
QPossibleValueSource possibleValueSource = QContext.getQInstance().getPossibleValueSource(possibleValueSourceName);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this looks complicated, but is just look for a label in the dropdown data and if found use it, //
|
||||
|
@ -23,11 +23,11 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
|
||||
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.AlertData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
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.dashboard.QWidgetMetaData;
|
||||
@ -40,9 +40,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
** - alertType - name of entry in AlertType enum (ERROR, WARNING, SUCCESS)
|
||||
** - alertHtml - html to display inside the alert (other than its icon)
|
||||
*******************************************************************************/
|
||||
public class ProcessAlertWidget extends AbstractWidgetRenderer implements MetaDataProducerInterface<QWidgetMetaData>
|
||||
public class AlertWidgetRenderer extends AbstractWidgetRenderer implements MetaDataProducerInterface<QWidgetMetaData>
|
||||
{
|
||||
public static final String NAME = "ProcessAlertWidget";
|
||||
public static final String NAME = "AlertWidgetRenderer";
|
||||
|
||||
|
||||
|
@ -34,8 +34,11 @@ import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
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.QNotFoundException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
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.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
@ -50,12 +53,15 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ChildRecordListData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
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.dashboard.AbstractWidgetMetaDataBuilder;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
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.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
@ -82,7 +88,9 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
.withIsCard(true)
|
||||
.withCodeReference(new QCodeReference(ChildRecordListRenderer.class))
|
||||
.withType(WidgetType.CHILD_RECORD_LIST.getType())
|
||||
.withDefaultValue("joinName", join.getName())));
|
||||
.withDefaultValue("joinName", join.getName())
|
||||
.withValidatorPlugin(new ChildRecordListWidgetValidator())
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@ -167,6 +175,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
widgetMetaData.withDefaultValue("manageAssociationName", manageAssociationName);
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -181,10 +190,10 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
String widgetLabel = input.getQueryParams().get("widgetLabel");
|
||||
String joinName = input.getQueryParams().get("joinName");
|
||||
QJoinMetaData join = input.getInstance().getJoin(joinName);
|
||||
QJoinMetaData join = QContext.getQInstance().getJoin(joinName);
|
||||
String id = input.getQueryParams().get("id");
|
||||
QTableMetaData leftTable = input.getInstance().getTable(join.getLeftTable());
|
||||
QTableMetaData rightTable = input.getInstance().getTable(join.getRightTable());
|
||||
QTableMetaData leftTable = QContext.getQInstance().getTable(join.getLeftTable());
|
||||
QTableMetaData rightTable = QContext.getQInstance().getTable(join.getRightTable());
|
||||
|
||||
Integer maxRows = null;
|
||||
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
|
||||
@ -193,7 +202,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
}
|
||||
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
|
||||
{
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().get("maxRows"));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -252,7 +261,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
}
|
||||
}
|
||||
|
||||
String tablePath = input.getInstance().getTablePath(rightTable.getName());
|
||||
String tablePath = QContext.getQInstance().getTablePath(rightTable.getName());
|
||||
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
|
||||
|
||||
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, rightTable, tablePath, viewAllLink, totalRows);
|
||||
@ -278,7 +287,9 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
|
||||
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
|
||||
{
|
||||
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<String> disabledFieldsForNewChildRecords = (Set<String>) widgetValues.get("disabledFieldsForNewChildRecords");
|
||||
widgetData.setDisabledFieldsForNewChildRecords(disabledFieldsForNewChildRecords);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -296,8 +307,18 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(widgetValues.containsKey("defaultValuesForNewChildRecordsFromParentFields"))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> defaultValuesForNewChildRecordsFromParentFields = (Map<String, String>) widgetValues.get("defaultValuesForNewChildRecordsFromParentFields");
|
||||
widgetData.setDefaultValuesForNewChildRecordsFromParentFields(defaultValuesForNewChildRecordsFromParentFields);
|
||||
}
|
||||
}
|
||||
|
||||
widgetData.setAllowRecordEdit(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("allowRecordEdit"))));
|
||||
widgetData.setAllowRecordDelete(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("allowRecordDelete"))));
|
||||
|
||||
return (new RenderWidgetOutput(widgetData));
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -307,4 +328,68 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static class ChildRecordListWidgetValidator implements QInstanceValidatorPluginInterface<QWidgetMetaDataInterface>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void validate(QWidgetMetaDataInterface widgetMetaData, QInstance qInstance, QInstanceValidator qInstanceValidator)
|
||||
{
|
||||
String prefix = "Widget " + widgetMetaData.getName() + ": ";
|
||||
|
||||
//////////////////////////////////
|
||||
// make sure join name is given //
|
||||
//////////////////////////////////
|
||||
String joinName = ValueUtils.getValueAsString(CollectionUtils.nonNullMap(widgetMetaData.getDefaultValues()).get("joinName"));
|
||||
if(qInstanceValidator.assertCondition(StringUtils.hasContent(joinName), prefix + "defaultValue for joinName must be given"))
|
||||
{
|
||||
///////////////////////////
|
||||
// make sure join exists //
|
||||
///////////////////////////
|
||||
QJoinMetaData join = qInstance.getJoin(joinName);
|
||||
if(qInstanceValidator.assertCondition(join != null, prefix + "No join named " + joinName + " exists in the instance"))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// if there's a manageAssociationName, make sure the table has that association //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
String manageAssociationName = ValueUtils.getValueAsString(widgetMetaData.getDefaultValues().get("manageAssociationName"));
|
||||
if(StringUtils.hasContent(manageAssociationName))
|
||||
{
|
||||
validateAssociationName(prefix, manageAssociationName, join, qInstance, qInstanceValidator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void validateAssociationName(String prefix, String manageAssociationName, QJoinMetaData join, QInstance qInstance, QInstanceValidator qInstanceValidator)
|
||||
{
|
||||
///////////////////////////////////
|
||||
// make sure join's table exists //
|
||||
///////////////////////////////////
|
||||
QTableMetaData table = qInstance.getTable(join.getLeftTable());
|
||||
if(table == null)
|
||||
{
|
||||
qInstanceValidator.getErrors().add(prefix + "Unable to validate manageAssociationName, as table [" + join.getLeftTable() + "] on left-side table of join [" + join.getName() + "] does not exist.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if(CollectionUtils.nonNullList(table.getAssociations()).stream().noneMatch(a -> manageAssociationName.equals(a.getName())))
|
||||
{
|
||||
qInstanceValidator.getErrors().add(prefix + "an association named [" + manageAssociationName + "] does not exist on table [" + join.getLeftTable() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
|
||||
|
||||
import java.util.HashMap;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
@ -57,7 +58,7 @@ public class ProcessWidgetRenderer extends AbstractWidgetRenderer
|
||||
setupDropdowns(input, widgetMetaData, data);
|
||||
|
||||
String processName = (String) widgetMetaData.getDefaultValues().get(WIDGET_PROCESS_NAME);
|
||||
QProcessMetaData processMetaData = input.getInstance().getProcess(processName);
|
||||
QProcessMetaData processMetaData = QContext.getQInstance().getProcess(processName);
|
||||
data.setProcessMetaData(processMetaData);
|
||||
|
||||
data.setDefaultValues(new HashMap<>(input.getQueryParams()));
|
||||
|
@ -0,0 +1,251 @@
|
||||
/*
|
||||
* 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.actions.dashboard.widgets;
|
||||
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
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.instances.QInstanceValidator;
|
||||
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.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.FilterUseCase;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
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.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ChildRecordListData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
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.dashboard.AbstractWidgetMetaDataBuilder;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Generic widget to display a list of records.
|
||||
**
|
||||
** Note, closely related to (and copied from ChildRecordListRenderer.
|
||||
** opportunity to share more code with that in the future??
|
||||
*******************************************************************************/
|
||||
public class RecordListWidgetRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(RecordListWidgetRenderer.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Builder widgetMetaDataBuilder(String widgetName)
|
||||
{
|
||||
return (new Builder(new QWidgetMetaData()
|
||||
.withName(widgetName)
|
||||
.withIsCard(true)
|
||||
.withCodeReference(new QCodeReference(RecordListWidgetRenderer.class))
|
||||
.withType(WidgetType.CHILD_RECORD_LIST.getType())
|
||||
.withValidatorPlugin(new RecordListWidgetValidator())
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class Builder extends AbstractWidgetMetaDataBuilder
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder(QWidgetMetaData widgetMetaData)
|
||||
{
|
||||
super(widgetMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withLabel(String label)
|
||||
{
|
||||
widgetMetaData.setLabel(label);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withMaxRows(Integer maxRows)
|
||||
{
|
||||
widgetMetaData.withDefaultValue("maxRows", maxRows);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withTableName(String tableName)
|
||||
{
|
||||
widgetMetaData.withDefaultValue("tableName", tableName);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withFilter(QQueryFilter filter)
|
||||
{
|
||||
widgetMetaData.withDefaultValue("filter", filter);
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
Integer maxRows = null;
|
||||
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
|
||||
{
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
|
||||
}
|
||||
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
|
||||
{
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().get("maxRows"));
|
||||
}
|
||||
|
||||
QQueryFilter filter = ((QQueryFilter) input.getWidgetMetaData().getDefaultValues().get("filter")).clone();
|
||||
filter.interpretValues(new HashMap<>(input.getQueryParams()), FilterUseCase.DEFAULT);
|
||||
filter.setLimit(maxRows);
|
||||
|
||||
String tableName = ValueUtils.getValueAsString(input.getWidgetMetaData().getDefaultValues().get("tableName"));
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(tableName);
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setShouldGenerateDisplayValues(true);
|
||||
queryInput.setFilter(filter);
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
QValueFormatter.setBlobValuesToDownloadUrls(table, queryOutput.getRecords());
|
||||
|
||||
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(tableName);
|
||||
countInput.setFilter(filter);
|
||||
totalRows = new CountAction().execute(countInput).getCount();
|
||||
}
|
||||
|
||||
String tablePath = QContext.getQInstance().getTablePath(tableName);
|
||||
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
|
||||
|
||||
ChildRecordListData widgetData = new ChildRecordListData(input.getQueryParams().get("widgetLabel"), queryOutput, table, tablePath, viewAllLink, totalRows);
|
||||
|
||||
return (new RenderWidgetOutput(widgetData));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error rendering record list widget", e, logPair("widgetName", () -> input.getWidgetMetaData().getName()));
|
||||
throw (e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static class RecordListWidgetValidator implements QInstanceValidatorPluginInterface<QWidgetMetaDataInterface>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void validate(QWidgetMetaDataInterface widgetMetaData, QInstance qInstance, QInstanceValidator qInstanceValidator)
|
||||
{
|
||||
String prefix = "Widget " + widgetMetaData.getName() + ": ";
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// make sure table name is given and exists //
|
||||
//////////////////////////////////////////////
|
||||
QTableMetaData table = null;
|
||||
String tableName = ValueUtils.getValueAsString(CollectionUtils.nonNullMap(widgetMetaData.getDefaultValues()).get("tableName"));
|
||||
if(qInstanceValidator.assertCondition(StringUtils.hasContent(tableName), prefix + "defaultValue for tableName must be given"))
|
||||
{
|
||||
////////////////////////////
|
||||
// make sure table exists //
|
||||
////////////////////////////
|
||||
table = qInstance.getTable(tableName);
|
||||
qInstanceValidator.assertCondition(table != null, prefix + "No table named " + tableName + " exists in the instance");
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// make sure filter is given and is valid (only check that if table is given too) //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
QQueryFilter filter = ((QQueryFilter) widgetMetaData.getDefaultValues().get("filter"));
|
||||
if(qInstanceValidator.assertCondition(filter != null, prefix + "defaultValue for filter must be given") && table != null)
|
||||
{
|
||||
qInstanceValidator.validateQueryFilter(qInstance, prefix, table, filter, null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
|
||||
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.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** a default implementation of MetaDataFilterInterface, that allows all the things
|
||||
*******************************************************************************/
|
||||
@Deprecated(since = "migrated to metaDataCustomizer")
|
||||
public class AllowAllMetaDataFilter implements MetaDataFilterInterface
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean allowTable(MetaDataInput input, QTableMetaData table)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean allowProcess(MetaDataInput input, QProcessMetaData process)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean allowReport(MetaDataInput input, QReportMetaData report)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean allowApp(MetaDataInput input, QAppMetaData app)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
|
||||
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.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** a default implementation of MetaDataFilterInterface, that is all noop.
|
||||
*******************************************************************************/
|
||||
public class DefaultNoopMetaDataActionCustomizer implements MetaDataActionCustomizerInterface
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean allowTable(MetaDataInput input, QTableMetaData table)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean allowProcess(MetaDataInput input, QProcessMetaData process)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean allowReport(MetaDataInput input, QReportMetaData report)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean allowApp(MetaDataInput input, QAppMetaData app)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
}
|
@ -23,17 +23,26 @@ package com.kingsrook.qqq.backend.core.actions.metadata;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
|
||||
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.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
|
||||
@ -48,6 +57,7 @@ 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.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -56,6 +66,12 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
*******************************************************************************/
|
||||
public class MetaDataAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(MetaDataAction.class);
|
||||
|
||||
private static Memoization<QInstance, MetaDataActionCustomizerInterface> metaDataActionCustomizerMemoization = new Memoization<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -63,27 +79,32 @@ public class MetaDataAction
|
||||
{
|
||||
ActionHelper.validateSession(metaDataInput);
|
||||
|
||||
// todo pre-customization - just get to modify the request?
|
||||
MetaDataOutput metaDataOutput = new MetaDataOutput();
|
||||
MetaDataOutput metaDataOutput = new MetaDataOutput();
|
||||
Map<String, AppTreeNode> treeNodes = new LinkedHashMap<>();
|
||||
|
||||
Map<String, AppTreeNode> treeNodes = new LinkedHashMap<>();
|
||||
MetaDataActionCustomizerInterface customizer = getMetaDataActionCustomizer();
|
||||
|
||||
/////////////////////////////////////
|
||||
// map tables to frontend metadata //
|
||||
/////////////////////////////////////
|
||||
Map<String, QFrontendTableMetaData> tables = new LinkedHashMap<>();
|
||||
for(Map.Entry<String, QTableMetaData> entry : metaDataInput.getInstance().getTables().entrySet())
|
||||
for(Map.Entry<String, QTableMetaData> entry : QContext.getQInstance().getTables().entrySet())
|
||||
{
|
||||
String tableName = entry.getKey();
|
||||
QTableMetaData table = entry.getValue();
|
||||
|
||||
if(!customizer.allowTable(metaDataInput, table))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, table);
|
||||
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName);
|
||||
QBackendMetaData backendForTable = QContext.getQInstance().getBackendForTable(tableName);
|
||||
tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false, false));
|
||||
treeNodes.put(tableName, new AppTreeNode(table));
|
||||
}
|
||||
@ -96,11 +117,16 @@ public class MetaDataAction
|
||||
// map processes to frontend metadata //
|
||||
////////////////////////////////////////
|
||||
Map<String, QFrontendProcessMetaData> processes = new LinkedHashMap<>();
|
||||
for(Map.Entry<String, QProcessMetaData> entry : metaDataInput.getInstance().getProcesses().entrySet())
|
||||
for(Map.Entry<String, QProcessMetaData> entry : QContext.getQInstance().getProcesses().entrySet())
|
||||
{
|
||||
String processName = entry.getKey();
|
||||
QProcessMetaData process = entry.getValue();
|
||||
|
||||
if(!customizer.allowProcess(metaDataInput, process))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, process);
|
||||
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
|
||||
{
|
||||
@ -116,11 +142,16 @@ public class MetaDataAction
|
||||
// map reports to frontend metadata //
|
||||
//////////////////////////////////////
|
||||
Map<String, QFrontendReportMetaData> reports = new LinkedHashMap<>();
|
||||
for(Map.Entry<String, QReportMetaData> entry : metaDataInput.getInstance().getReports().entrySet())
|
||||
for(Map.Entry<String, QReportMetaData> entry : QContext.getQInstance().getReports().entrySet())
|
||||
{
|
||||
String reportName = entry.getKey();
|
||||
QReportMetaData report = entry.getValue();
|
||||
|
||||
if(!customizer.allowReport(metaDataInput, report))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, report);
|
||||
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
|
||||
{
|
||||
@ -136,11 +167,16 @@ public class MetaDataAction
|
||||
// map widgets to frontend metadata //
|
||||
//////////////////////////////////////
|
||||
Map<String, QFrontendWidgetMetaData> widgets = new LinkedHashMap<>();
|
||||
for(Map.Entry<String, QWidgetMetaDataInterface> entry : metaDataInput.getInstance().getWidgets().entrySet())
|
||||
for(Map.Entry<String, QWidgetMetaDataInterface> entry : QContext.getQInstance().getWidgets().entrySet())
|
||||
{
|
||||
String widgetName = entry.getKey();
|
||||
QWidgetMetaDataInterface widget = entry.getValue();
|
||||
|
||||
if(!customizer.allowWidget(metaDataInput, widget))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, widget);
|
||||
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
|
||||
{
|
||||
@ -154,7 +190,7 @@ public class MetaDataAction
|
||||
///////////////////////////////////////////////////////
|
||||
// sort apps - by sortOrder (integer), then by label //
|
||||
///////////////////////////////////////////////////////
|
||||
List<QAppMetaData> sortedApps = metaDataInput.getInstance().getApps().values().stream()
|
||||
List<QAppMetaData> sortedApps = QContext.getQInstance().getApps().values().stream()
|
||||
.sorted(Comparator.comparing((QAppMetaData a) -> a.getSortOrder())
|
||||
.thenComparing((QAppMetaData a) -> a.getLabel()))
|
||||
.toList();
|
||||
@ -173,9 +209,19 @@ public class MetaDataAction
|
||||
continue;
|
||||
}
|
||||
|
||||
apps.put(appName, new QFrontendAppMetaData(app, metaDataOutput));
|
||||
treeNodes.put(appName, new AppTreeNode(app));
|
||||
if(!customizer.allowApp(metaDataInput, app))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
// build the frontend-app meta-data //
|
||||
//////////////////////////////////////
|
||||
QFrontendAppMetaData frontendAppMetaData = new QFrontendAppMetaData(app, metaDataOutput);
|
||||
|
||||
/////////////////////////////////////////
|
||||
// add children (if they're permitted) //
|
||||
/////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(app.getChildren()))
|
||||
{
|
||||
for(QAppChildMetaData child : app.getChildren())
|
||||
@ -189,9 +235,42 @@ public class MetaDataAction
|
||||
}
|
||||
}
|
||||
|
||||
apps.get(appName).addChild(new AppTreeNode(child));
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the child was filtered away, so it isn't in its corresponding map, then don't include it here //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(child instanceof QTableMetaData table && !tables.containsKey(table.getName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if(child instanceof QProcessMetaData process && !processes.containsKey(process.getName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if(child instanceof QReportMetaData report && !reports.containsKey(report.getName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if(child instanceof QAppMetaData childApp && !apps.containsKey(childApp.getName()))
|
||||
{
|
||||
// continue;
|
||||
}
|
||||
|
||||
frontendAppMetaData.addChild(new AppTreeNode(child));
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the app ended up having no children, then discard it //
|
||||
// todo - i think this was wrong, because it didn't take into account ... something nested maybe... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeIsEmpty(frontendAppMetaData.getChildren()) && CollectionUtils.nullSafeIsEmpty(frontendAppMetaData.getWidgets()))
|
||||
{
|
||||
// LOG.debug("Discarding empty app", logPair("name", frontendAppMetaData.getName()));
|
||||
// continue;
|
||||
}
|
||||
|
||||
apps.put(appName, frontendAppMetaData);
|
||||
treeNodes.put(appName, new AppTreeNode(app));
|
||||
}
|
||||
metaDataOutput.setApps(apps);
|
||||
|
||||
@ -211,22 +290,70 @@ public class MetaDataAction
|
||||
////////////////////////////////////
|
||||
// add branding metadata if found //
|
||||
////////////////////////////////////
|
||||
if(metaDataInput.getInstance().getBranding() != null)
|
||||
if(QContext.getQInstance().getBranding() != null)
|
||||
{
|
||||
metaDataOutput.setBranding(metaDataInput.getInstance().getBranding());
|
||||
metaDataOutput.setBranding(QContext.getQInstance().getBranding());
|
||||
}
|
||||
|
||||
metaDataOutput.setEnvironmentValues(metaDataInput.getInstance().getEnvironmentValues());
|
||||
metaDataOutput.setEnvironmentValues(Objects.requireNonNullElse(QContext.getQInstance().getEnvironmentValues(), Collections.emptyMap()));
|
||||
|
||||
metaDataOutput.setHelpContents(metaDataInput.getInstance().getHelpContent());
|
||||
metaDataOutput.setHelpContents(Objects.requireNonNullElse(QContext.getQInstance().getHelpContent(), Collections.emptyMap()));
|
||||
|
||||
// todo post-customization - can do whatever w/ the result if you want?
|
||||
try
|
||||
{
|
||||
customizer.postProcess(metaDataOutput);
|
||||
}
|
||||
catch(QUserFacingException e)
|
||||
{
|
||||
LOG.debug("User-facing exception thrown in meta-data customizer post-processing", e);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Unexpected error thrown in meta-data customizer post-processing", e);
|
||||
}
|
||||
|
||||
return metaDataOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private MetaDataActionCustomizerInterface getMetaDataActionCustomizer()
|
||||
{
|
||||
return metaDataActionCustomizerMemoization.getResult(QContext.getQInstance(), i ->
|
||||
{
|
||||
MetaDataActionCustomizerInterface actionCustomizer = null;
|
||||
QCodeReference metaDataActionCustomizerReference = QContext.getQInstance().getMetaDataActionCustomizer();
|
||||
if(metaDataActionCustomizerReference != null)
|
||||
{
|
||||
actionCustomizer = QCodeLoader.getAdHoc(MetaDataActionCustomizerInterface.class, metaDataActionCustomizerReference);
|
||||
LOG.debug("Using new meta-data actionCustomizer of type: " + actionCustomizer.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
if(actionCustomizer == null)
|
||||
{
|
||||
QCodeReference metaDataFilterReference = QContext.getQInstance().getMetaDataFilter();
|
||||
if(metaDataFilterReference != null)
|
||||
{
|
||||
actionCustomizer = QCodeLoader.getAdHoc(MetaDataActionCustomizerInterface.class, metaDataFilterReference);
|
||||
LOG.debug("Using new meta-data actionCustomizer (via metaDataFilter reference) of type: " + actionCustomizer.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
if(actionCustomizer == null)
|
||||
{
|
||||
actionCustomizer = new DefaultNoopMetaDataActionCustomizer();
|
||||
LOG.debug("Using new default (allow-all) meta-data actionCustomizer");
|
||||
}
|
||||
|
||||
return (actionCustomizer);
|
||||
}).orElseThrow(() -> new QRuntimeException("Error getting MetaDataActionCustomizer"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
|
||||
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.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Interface for customizations that can be injected by an application into
|
||||
** the MetaDataAction - e.g., loading applicable meta-data for a user into a
|
||||
** frontend.
|
||||
*******************************************************************************/
|
||||
public interface MetaDataActionCustomizerInterface
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
boolean allowTable(MetaDataInput input, QTableMetaData table);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
boolean allowProcess(MetaDataInput input, QProcessMetaData process);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
boolean allowReport(MetaDataInput input, QReportMetaData report);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
boolean allowApp(MetaDataInput input, QAppMetaData app);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
boolean allowWidget(MetaDataInput input, QWidgetMetaDataInterface widget);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
default void postProcess(MetaDataOutput metaDataOutput) throws QException
|
||||
{
|
||||
/////////////////////
|
||||
// noop by default //
|
||||
/////////////////////
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.metadata;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Deprecated(since = "migrated to metaDataCustomizer")
|
||||
public interface MetaDataFilterInterface extends MetaDataActionCustomizerInterface
|
||||
{
|
||||
|
||||
}
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInput;
|
||||
@ -47,7 +48,7 @@ public class ProcessMetaDataAction
|
||||
// todo pre-customization - just get to modify the request?
|
||||
ProcessMetaDataOutput processMetaDataOutput = new ProcessMetaDataOutput();
|
||||
|
||||
QProcessMetaData process = processMetaDataInput.getInstance().getProcess(processMetaDataInput.getProcessName());
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processMetaDataInput.getProcessName());
|
||||
if(process == null)
|
||||
{
|
||||
throw (new QNotFoundException("Process [" + processMetaDataInput.getProcessName() + "] was not found."));
|
||||
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
|
||||
@ -48,12 +49,12 @@ public class TableMetaDataAction
|
||||
// todo pre-customization - just get to modify the request?
|
||||
TableMetaDataOutput tableMetaDataOutput = new TableMetaDataOutput();
|
||||
|
||||
QTableMetaData table = tableMetaDataInput.getInstance().getTable(tableMetaDataInput.getTableName());
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableMetaDataInput.getTableName());
|
||||
if(table == null)
|
||||
{
|
||||
throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found."));
|
||||
}
|
||||
QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName());
|
||||
QBackendMetaData backendForTable = QContext.getQInstance().getBackendForTable(table.getName());
|
||||
tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true, true));
|
||||
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
|
@ -49,6 +49,7 @@ 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;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -78,6 +79,12 @@ public class PermissionsHelper
|
||||
warnAboutPermissionSubTypeForTables(permissionSubType);
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
|
||||
if(table == null)
|
||||
{
|
||||
LOG.info("Throwing a permission denied exception in response to a non-existent table name", logPair("tableName", tableName));
|
||||
throw (new QPermissionDeniedException("Permission denied."));
|
||||
}
|
||||
|
||||
commonCheckPermissionThrowing(getEffectivePermissionRules(table, QContext.getQInstance()), permissionSubType, table.getName());
|
||||
}
|
||||
|
||||
@ -184,7 +191,14 @@ public class PermissionsHelper
|
||||
*******************************************************************************/
|
||||
public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName, Map<String, Serializable> processValues) throws QPermissionDeniedException
|
||||
{
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
|
||||
if(process == null)
|
||||
{
|
||||
LOG.info("Throwing a permission denied exception in response to a non-existent process name", logPair("processName", processName));
|
||||
throw (new QPermissionDeniedException("Permission denied."));
|
||||
}
|
||||
|
||||
QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, QContext.getQInstance());
|
||||
|
||||
if(effectivePermissionRules.getCustomPermissionChecker() != null)
|
||||
@ -226,6 +240,13 @@ public class PermissionsHelper
|
||||
public static void checkAppPermissionThrowing(AbstractActionInput actionInput, String appName) throws QPermissionDeniedException
|
||||
{
|
||||
QAppMetaData app = QContext.getQInstance().getApp(appName);
|
||||
|
||||
if(app == null)
|
||||
{
|
||||
LOG.info("Throwing a permission denied exception in response to a non-existent app name", logPair("appName", appName));
|
||||
throw (new QPermissionDeniedException("Permission denied."));
|
||||
}
|
||||
|
||||
commonCheckPermissionThrowing(getEffectivePermissionRules(app, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName());
|
||||
}
|
||||
|
||||
@ -255,6 +276,13 @@ public class PermissionsHelper
|
||||
public static void checkReportPermissionThrowing(AbstractActionInput actionInput, String reportName) throws QPermissionDeniedException
|
||||
{
|
||||
QReportMetaData report = QContext.getQInstance().getReport(reportName);
|
||||
|
||||
if(report == null)
|
||||
{
|
||||
LOG.info("Throwing a permission denied exception in response to a non-existent process name", logPair("reportName", reportName));
|
||||
throw (new QPermissionDeniedException("Permission denied."));
|
||||
}
|
||||
|
||||
commonCheckPermissionThrowing(getEffectivePermissionRules(report, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName());
|
||||
}
|
||||
|
||||
@ -284,6 +312,13 @@ public class PermissionsHelper
|
||||
public static void checkWidgetPermissionThrowing(AbstractActionInput actionInput, String widgetName) throws QPermissionDeniedException
|
||||
{
|
||||
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
|
||||
|
||||
if(widget == null)
|
||||
{
|
||||
LOG.info("Throwing a permission denied exception in response to a non-existent widget name", logPair("widgetName", widgetName));
|
||||
throw (new QPermissionDeniedException("Permission denied."));
|
||||
}
|
||||
|
||||
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName());
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.processes;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
@ -54,7 +55,7 @@ public class CancelProcessAction extends RunProcessAction
|
||||
{
|
||||
ActionHelper.validateSession(runProcessInput);
|
||||
|
||||
QProcessMetaData process = runProcessInput.getInstance().getProcess(runProcessInput.getProcessName());
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(runProcessInput.getProcessName());
|
||||
if(process == null)
|
||||
{
|
||||
throw new QBadRequestException("Process [" + runProcessInput.getProcessName() + "] is not defined in this instance.");
|
||||
|
@ -30,20 +30,25 @@ import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
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.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
@ -64,7 +69,7 @@ public class RunBackendStepAction
|
||||
{
|
||||
ActionHelper.validateSession(runBackendStepInput);
|
||||
|
||||
QProcessMetaData process = runBackendStepInput.getInstance().getProcess(runBackendStepInput.getProcessName());
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(runBackendStepInput.getProcessName());
|
||||
if(process == null)
|
||||
{
|
||||
throw new QException("Process [" + runBackendStepInput.getProcessName() + "] is not defined in this instance.");
|
||||
@ -186,21 +191,40 @@ public class RunBackendStepAction
|
||||
{
|
||||
if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(inputMetaData.getRecordListMetaData().getTableName());
|
||||
QTableMetaData table = QContext.getQInstance().getTable(inputMetaData.getRecordListMetaData().getTableName());
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(table.getName());
|
||||
|
||||
// todo - handle this being async (e.g., http)
|
||||
// seems like it just needs to throw, breaking this flow, and to send a response to the frontend, directing it to prompt the user for the needed data
|
||||
// then this step can re-run, hopefully with the needed data.
|
||||
|
||||
QProcessCallback callback = runBackendStepInput.getCallback();
|
||||
if(callback == null)
|
||||
//////////////////////////////////////////////////
|
||||
// look for record ids in the input data values //
|
||||
//////////////////////////////////////////////////
|
||||
String recordIds = (String) runBackendStepInput.getValue("recordIds");
|
||||
if(recordIds == null)
|
||||
{
|
||||
throw (new QUserFacingException("Missing input records.",
|
||||
new QException("Function is missing input records, but no callback was present to request fields from a user")));
|
||||
recordIds = (String) runBackendStepInput.getValue("recordId");
|
||||
}
|
||||
|
||||
queryInput.setFilter(callback.getQueryFilter());
|
||||
///////////////////////////////////////////////////////////
|
||||
// if records were found, add as criteria to query input //
|
||||
///////////////////////////////////////////////////////////
|
||||
if(recordIds != null)
|
||||
{
|
||||
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordIds.split(","))));
|
||||
}
|
||||
else
|
||||
{
|
||||
// todo - handle this being async (e.g., http)
|
||||
// seems like it just needs to throw, breaking this flow, and to send a response to the frontend, directing it to prompt the user for the needed data
|
||||
// then this step can re-run, hopefully with the needed data.
|
||||
QProcessCallback callback = runBackendStepInput.getCallback();
|
||||
if(callback == null)
|
||||
{
|
||||
throw (new QUserFacingException("Missing input records.",
|
||||
new QException("Function is missing input records, but no callback was present to request fields from a user")));
|
||||
}
|
||||
|
||||
queryInput.setFilter(callback.getQueryFilter());
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if process has a max-no of records, set a limit on the process of that number plus 1 //
|
||||
@ -208,7 +232,7 @@ public class RunBackendStepAction
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(process.getMaxInputRecords() != null)
|
||||
{
|
||||
if(callback.getQueryFilter() == null)
|
||||
if(queryInput.getFilter() == null)
|
||||
{
|
||||
queryInput.setFilter(new QQueryFilter());
|
||||
}
|
||||
@ -256,11 +280,20 @@ public class RunBackendStepAction
|
||||
{
|
||||
runBackendStepOutput.seedFromRequest(runBackendStepInput);
|
||||
|
||||
Class<?> codeClass = Class.forName(code.getName());
|
||||
Object codeObject = codeClass.getConstructor().newInstance();
|
||||
Object codeObject;
|
||||
if(code instanceof QCodeReferenceLambda<?> qCodeReferenceLambda)
|
||||
{
|
||||
codeObject = qCodeReferenceLambda.getLambda();
|
||||
}
|
||||
else
|
||||
{
|
||||
Class<?> codeClass = Class.forName(code.getName());
|
||||
codeObject = codeClass.getConstructor().newInstance();
|
||||
}
|
||||
|
||||
if(!(codeObject instanceof BackendStep backendStepCodeObject))
|
||||
{
|
||||
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of BackendStep"));
|
||||
throw (new QException("The supplied codeReference [" + code + "] is not a reference to a BackendStep"));
|
||||
}
|
||||
|
||||
backendStepCodeObject.run(runBackendStepInput, runBackendStepOutput);
|
||||
|
@ -28,9 +28,11 @@ import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.NoCodeWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
@ -52,15 +54,18 @@ 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.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
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.NoCodeWidgetFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
|
||||
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerInterface;
|
||||
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
|
||||
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
|
||||
import com.kingsrook.qqq.backend.core.state.StateType;
|
||||
@ -69,6 +74,7 @@ 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.commons.lang.BooleanUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -79,17 +85,22 @@ public class RunProcessAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(RunProcessAction.class);
|
||||
|
||||
public static final String BASEPULL_KEY_VALUE = "basepullKeyValue";
|
||||
public static final String BASEPULL_THIS_RUNTIME_KEY = "basepullThisRuntimeKey";
|
||||
public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey";
|
||||
public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField";
|
||||
public static final String BASEPULL_CONFIGURATION = "basepullConfiguration";
|
||||
|
||||
public static final String PROCESS_TRACER_CODE_REFERENCE_FIELD = "processTracerCodeReference";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// indicator that the timestamp field should be updated - e.g., the execute step is finished. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
public static final String BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD = "basepullReadyToUpdateTimestamp";
|
||||
public static final String BASEPULL_DID_QUERY_USING_TIMESTAMP_FIELD = "basepullDidQueryUsingTimestamp";
|
||||
|
||||
private ProcessTracerInterface processTracer;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -99,7 +110,7 @@ public class RunProcessAction
|
||||
{
|
||||
ActionHelper.validateSession(runProcessInput);
|
||||
|
||||
QProcessMetaData process = runProcessInput.getInstance().getProcess(runProcessInput.getProcessName());
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(runProcessInput.getProcessName());
|
||||
if(process == null)
|
||||
{
|
||||
throw new QException("Process [" + runProcessInput.getProcessName() + "] is not defined in this instance.");
|
||||
@ -116,9 +127,17 @@ public class RunProcessAction
|
||||
}
|
||||
runProcessOutput.setProcessUUID(runProcessInput.getProcessUUID());
|
||||
|
||||
traceStartOrResume(runProcessInput, process);
|
||||
|
||||
UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS);
|
||||
ProcessState processState = primeProcessState(runProcessInput, stateKey, process);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// these should always be clear when we're starting a run - so make sure they haven't leaked from previous //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
processState.clearNextStepName();
|
||||
processState.clearBackStepName();
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// if process is 'basepull' style, keep track of 'now' //
|
||||
/////////////////////////////////////////////////////////
|
||||
@ -133,90 +152,11 @@ public class RunProcessAction
|
||||
|
||||
try
|
||||
{
|
||||
String lastStepName = runProcessInput.getStartAfterStep();
|
||||
|
||||
STEP_LOOP:
|
||||
while(true)
|
||||
switch(Objects.requireNonNull(process.getStepFlow(), "Process [" + process.getName() + "] has a null stepFlow."))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// always refresh the step list - as any step that runs can modify it (in the process state). //
|
||||
// this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QStepMetaData> stepList = getAvailableStepList(processState, process, lastStepName);
|
||||
if(stepList.isEmpty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
QStepMetaData step = stepList.get(0);
|
||||
lastStepName = step.getName();
|
||||
|
||||
if(step instanceof QFrontendStepMetaData frontendStep)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////
|
||||
// Handle what to do with frontend steps, per request setting //
|
||||
////////////////////////////////////////////////////////////////
|
||||
switch(runProcessInput.getFrontendStepBehavior())
|
||||
{
|
||||
case BREAK ->
|
||||
{
|
||||
LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
|
||||
processFrontendStepFieldDefaultValues(processState, frontendStep);
|
||||
processFrontendComponents(processState, frontendStep);
|
||||
processState.setNextStepName(step.getName());
|
||||
break STEP_LOOP;
|
||||
}
|
||||
case SKIP ->
|
||||
{
|
||||
LOG.trace("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// much less error prone in case this code changes in the future... //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// noinspection UnnecessaryContinue
|
||||
continue;
|
||||
}
|
||||
case FAIL ->
|
||||
{
|
||||
LOG.trace("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
|
||||
throw (new QException("Failing process at step " + step.getName() + " (as requested, to fail on frontend steps)"));
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected value: " + runProcessInput.getFrontendStepBehavior());
|
||||
}
|
||||
}
|
||||
else if(step instanceof QBackendStepMetaData backendStepMetaData)
|
||||
{
|
||||
///////////////////////
|
||||
// Run backend steps //
|
||||
///////////////////////
|
||||
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
|
||||
RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the step returned an override lastStepName, use that to determine how we proceed //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(runBackendStepOutput.getOverrideLastStepName() != null)
|
||||
{
|
||||
LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!");
|
||||
lastStepName = runBackendStepOutput.getOverrideLastStepName();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// similarly, if the step produced an updatedFrontendStepList, propagate that data outward //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(runBackendStepOutput.getUpdatedFrontendStepList() != null)
|
||||
{
|
||||
LOG.debug("Process step [" + lastStepName + "] generated an updatedFrontendStepList [" + runBackendStepOutput.getUpdatedFrontendStepList().stream().map(s -> s.getName()).toList() + "]!");
|
||||
runProcessOutput.setUpdatedFrontendStepList(runBackendStepOutput.getUpdatedFrontendStepList());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////
|
||||
// in case we have a different step type, throw //
|
||||
//////////////////////////////////////////////////
|
||||
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
|
||||
}
|
||||
case LINEAR -> runLinearStepLoop(process, processState, stateKey, runProcessInput, runProcessOutput);
|
||||
case STATE_MACHINE -> runStateMachineStep(runProcessInput.getStartAfterStep(), process, processState, stateKey, runProcessInput, runProcessOutput, 0);
|
||||
default -> throw (new QException("Unhandled process step flow: " + process.getStepFlow()));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@ -236,6 +176,7 @@ public class RunProcessAction
|
||||
////////////////////////////////////////////////////////////
|
||||
// upon exception (e.g., one thrown by a step), throw it. //
|
||||
////////////////////////////////////////////////////////////
|
||||
traceBreakOrFinish(runProcessInput, runProcessOutput, qe);
|
||||
throw (qe);
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -243,6 +184,7 @@ public class RunProcessAction
|
||||
////////////////////////////////////////////////////////////
|
||||
// upon exception (e.g., one thrown by a step), throw it. //
|
||||
////////////////////////////////////////////////////////////
|
||||
traceBreakOrFinish(runProcessInput, runProcessOutput, e);
|
||||
throw (new QException("Error running process", e));
|
||||
}
|
||||
finally
|
||||
@ -253,11 +195,317 @@ public class RunProcessAction
|
||||
runProcessOutput.setProcessState(processState);
|
||||
}
|
||||
|
||||
traceBreakOrFinish(runProcessInput, runProcessOutput, null);
|
||||
|
||||
return (runProcessOutput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void runLinearStepLoop(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws Exception
|
||||
{
|
||||
String lastStepName = runProcessInput.getStartAfterStep();
|
||||
String startAtStep = runProcessInput.getStartAtStep();
|
||||
|
||||
while(true)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// always refresh the step list - as any step that runs can modify it (in the process state). //
|
||||
// this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. //
|
||||
// deal with if we were told, from the input, to start After a step, or start At a step. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QStepMetaData> stepList;
|
||||
if(startAtStep == null)
|
||||
{
|
||||
stepList = getAvailableStepList(processState, process, lastStepName, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
stepList = getAvailableStepList(processState, process, startAtStep, true);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// clear this field - so after we run a step, we'll then loop in last-step mode. //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
startAtStep = null;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// if we're going to run a backend step now, let it see that this is a step-back //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
processState.setIsStepBack(true);
|
||||
}
|
||||
|
||||
if(stepList.isEmpty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
QStepMetaData step = stepList.get(0);
|
||||
lastStepName = step.getName();
|
||||
|
||||
if(step instanceof QFrontendStepMetaData frontendStep)
|
||||
{
|
||||
LoopTodo loopTodo = prepareForFrontendStep(runProcessInput, process, frontendStep, processState);
|
||||
if(loopTodo == LoopTodo.BREAK)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if(step instanceof QBackendStepMetaData backendStepMetaData)
|
||||
{
|
||||
RunBackendStepOutput runBackendStepOutput = runBackendStep(process, processState, stateKey, runProcessInput, runProcessOutput, backendStepMetaData, step);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the step returned an override lastStepName, use that to determine how we proceed //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(runBackendStepOutput.getOverrideLastStepName() != null)
|
||||
{
|
||||
LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!");
|
||||
lastStepName = runBackendStepOutput.getOverrideLastStepName();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////
|
||||
// in case we have a different step type, throw //
|
||||
//////////////////////////////////////////////////
|
||||
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// only let this value be set for the original back step - don't let it stick around. //
|
||||
// if a process wants to keep track of this itself, it can, but in a different slot. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
processState.setIsStepBack(false);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// in case we broke from the loop above (e.g., by going directly into a frontend step), once again make sure to lower this flag. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
processState.setIsStepBack(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private enum LoopTodo
|
||||
{
|
||||
BREAK,
|
||||
CONTINUE
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private LoopTodo prepareForFrontendStep(RunProcessInput runProcessInput, QProcessMetaData process, QFrontendStepMetaData step, ProcessState processState) throws QException
|
||||
{
|
||||
////////////////////////////////////////////////////////////////
|
||||
// Handle what to do with frontend steps, per request setting //
|
||||
////////////////////////////////////////////////////////////////
|
||||
switch(runProcessInput.getFrontendStepBehavior())
|
||||
{
|
||||
case BREAK ->
|
||||
{
|
||||
LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
|
||||
processFrontendStepFieldDefaultValues(processState, step);
|
||||
processFrontendComponents(processState, step);
|
||||
processState.setNextStepName(step.getName());
|
||||
|
||||
if(StringUtils.hasContent(step.getBackStepName()) && processState.getBackStepName().isEmpty())
|
||||
{
|
||||
processState.setBackStepName(step.getBackStepName());
|
||||
}
|
||||
|
||||
return LoopTodo.BREAK;
|
||||
}
|
||||
case SKIP ->
|
||||
{
|
||||
LOG.trace("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
|
||||
return LoopTodo.CONTINUE;
|
||||
}
|
||||
case FAIL ->
|
||||
{
|
||||
LOG.trace("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
|
||||
throw (new QException("Failing process at step " + step.getName() + " (as requested, to fail on frontend steps)"));
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected value: " + runProcessInput.getFrontendStepBehavior());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void runStateMachineStep(String lastStepName, QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, int stackDepth) throws Exception
|
||||
{
|
||||
//////////////////////////////
|
||||
// check for stack-overflow //
|
||||
//////////////////////////////
|
||||
Integer maxStateMachineProcessStepFlowStackDepth = Objects.requireNonNullElse(runProcessInput.getValueInteger("maxStateMachineProcessStepFlowStackDepth"), 20);
|
||||
if(stackDepth > maxStateMachineProcessStepFlowStackDepth)
|
||||
{
|
||||
throw (new QException("StateMachine process recurred too many times (exceeded maxStateMachineProcessStepFlowStackDepth of " + maxStateMachineProcessStepFlowStackDepth + ")"));
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// figure out what step to run: //
|
||||
//////////////////////////////////
|
||||
QStepMetaData step = null;
|
||||
if(!StringUtils.hasContent(lastStepName))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// if no lastStepName is given, start at the process's first step //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeIsEmpty(process.getStepList()))
|
||||
{
|
||||
throw (new QException("Process [" + process.getName() + "] does not have a step list defined."));
|
||||
}
|
||||
step = process.getStepList().get(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////
|
||||
// else run the given lastStepName //
|
||||
/////////////////////////////////////
|
||||
processState.clearNextStepName();
|
||||
processState.clearBackStepName();
|
||||
step = process.getStep(lastStepName);
|
||||
if(step == null)
|
||||
{
|
||||
throw (new QException("Could not find step by name [" + lastStepName + "]"));
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// for the flow of: //
|
||||
// we were on a frontend step (as a sub-step of a state machine step), //
|
||||
// and now we're here to run that state-step's backend step - //
|
||||
// find the state-machine step containing this frontend step. //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
String skipSubStepsUntil = null;
|
||||
if(step instanceof QFrontendStepMetaData frontendStepMetaData)
|
||||
{
|
||||
QStateMachineStep stateMachineStep = getStateMachineStepContainingSubStep(process, frontendStepMetaData.getName());
|
||||
if(stateMachineStep == null)
|
||||
{
|
||||
throw (new QException("Could not find stateMachineStep that contains last-frontend step: " + frontendStepMetaData.getName()));
|
||||
}
|
||||
step = stateMachineStep;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// set this flag, to know to skip this frontend step in the sub-step loop below //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
skipSubStepsUntil = frontendStepMetaData.getName();
|
||||
}
|
||||
|
||||
if(!(step instanceof QStateMachineStep stateMachineStep))
|
||||
{
|
||||
throw (new QException("Have a non-stateMachineStep in a process using stateMachine flow... " + step.getClass().getName()));
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
// run the sub-steps //
|
||||
///////////////////////
|
||||
boolean ranAnySubSteps = false;
|
||||
for(QStepMetaData subStep : stateMachineStep.getSubSteps())
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok, well, skip them if this flag is set (and clear the flag once we've hit this sub-step) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(skipSubStepsUntil != null)
|
||||
{
|
||||
if(skipSubStepsUntil.equals(subStep.getName()))
|
||||
{
|
||||
skipSubStepsUntil = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
ranAnySubSteps = true;
|
||||
if(subStep instanceof QFrontendStepMetaData frontendStep)
|
||||
{
|
||||
LoopTodo loopTodo = prepareForFrontendStep(runProcessInput, process, frontendStep, processState);
|
||||
if(loopTodo == LoopTodo.BREAK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if(subStep instanceof QBackendStepMetaData backendStepMetaData)
|
||||
{
|
||||
RunBackendStepOutput runBackendStepOutput = runBackendStep(process, processState, stateKey, runProcessInput, runProcessOutput, backendStepMetaData, step);
|
||||
Optional<String> nextStepName = runBackendStepOutput.getProcessState().getNextStepName();
|
||||
|
||||
if(nextStepName.isEmpty() && StringUtils.hasContent(stateMachineStep.getDefaultNextStepName()))
|
||||
{
|
||||
nextStepName = Optional.of(stateMachineStep.getDefaultNextStepName());
|
||||
}
|
||||
|
||||
if(nextStepName.isPresent())
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we've been given a next-step-name, go to that step now. //
|
||||
// it might be a backend-only stateMachineStep, in which case, we should run that backend step now. //
|
||||
// or it might be a frontend-then-backend step, in which case, we want to go to that frontend step. //
|
||||
// if we weren't given a next-step-name, then we should stay in the same state - either to finish //
|
||||
// its sub-steps, or, to fall out of the loop and end the process. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
processState.clearNextStepName();
|
||||
processState.clearBackStepName();
|
||||
runStateMachineStep(nextStepName.get(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////
|
||||
// in case we have a different step type, throw //
|
||||
//////////////////////////////////////////////////
|
||||
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
|
||||
}
|
||||
}
|
||||
|
||||
if(!ranAnySubSteps)
|
||||
{
|
||||
if(StringUtils.hasContent(stateMachineStep.getDefaultNextStepName()))
|
||||
{
|
||||
runStateMachineStep(stateMachineStep.getDefaultNextStepName(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QStateMachineStep getStateMachineStepContainingSubStep(QProcessMetaData process, String stepName)
|
||||
{
|
||||
for(QStepMetaData step : process.getAllSteps().values())
|
||||
{
|
||||
if(step instanceof QStateMachineStep stateMachineStep)
|
||||
{
|
||||
for(QStepMetaData subStep : stateMachineStep.getSubSteps())
|
||||
{
|
||||
if(subStep.getName().equals(stepName))
|
||||
{
|
||||
return (stateMachineStep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -335,12 +583,12 @@ public class RunProcessAction
|
||||
///////////////////////////////////////////////////
|
||||
runProcessInput.seedFromProcessState(optionalProcessState.get());
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we're restoring an old state, we can discard a previously stored updatedFrontendStepList - //
|
||||
// it is only needed on the transitional edge from a backend-step to a frontend step, but not //
|
||||
// in the other directly //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
optionalProcessState.get().setUpdatedFrontendStepList(null);
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we're restoring an old state, we can discard a previously stored processMetaDataAdjustment - //
|
||||
// it is only needed on the transitional edge from a backend-step to a frontend step, but not //
|
||||
// in the other directly //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
optionalProcessState.get().setProcessMetaDataAdjustment(null);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// if there were values from the caller, put those (back) in the request //
|
||||
@ -355,16 +603,40 @@ public class RunProcessAction
|
||||
}
|
||||
|
||||
ProcessState processState = optionalProcessState.get();
|
||||
processState.clearNextStepName();
|
||||
return processState;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private RunBackendStepOutput runBackendStep(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, QBackendStepMetaData backendStepMetaData, QStepMetaData step) throws Exception
|
||||
{
|
||||
///////////////////////
|
||||
// Run backend steps //
|
||||
///////////////////////
|
||||
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
|
||||
RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// similarly, if the step produced a processMetaDataAdjustment, propagate that data outward //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(runBackendStepOutput.getProcessMetaDataAdjustment() != null)
|
||||
{
|
||||
LOG.debug("Process step [" + step.getName() + "] generated a ProcessMetaDataAdjustment [" + runBackendStepOutput.getProcessMetaDataAdjustment() + "]!");
|
||||
runProcessOutput.setProcessMetaDataAdjustment(runBackendStepOutput.getProcessMetaDataAdjustment());
|
||||
}
|
||||
|
||||
return runBackendStepOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run a single backend step.
|
||||
*******************************************************************************/
|
||||
protected RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
|
||||
RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
|
||||
{
|
||||
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
|
||||
runBackendStepInput.setProcessName(process.getName());
|
||||
@ -372,6 +644,7 @@ public class RunProcessAction
|
||||
runBackendStepInput.setCallback(runProcessInput.getCallback());
|
||||
runBackendStepInput.setFrontendStepBehavior(runProcessInput.getFrontendStepBehavior());
|
||||
runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback());
|
||||
runBackendStepInput.setProcessTracer(processTracer);
|
||||
|
||||
runBackendStepInput.setTableName(process.getTableName());
|
||||
if(!StringUtils.hasContent(runBackendStepInput.getTableName()))
|
||||
@ -393,9 +666,13 @@ public class RunProcessAction
|
||||
runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY));
|
||||
}
|
||||
|
||||
traceStepStart(runBackendStepInput);
|
||||
|
||||
RunBackendStepOutput runBackendStepOutput = new RunBackendStepAction().execute(runBackendStepInput);
|
||||
storeState(stateKey, runBackendStepOutput.getProcessState());
|
||||
|
||||
traceStepFinish(runBackendStepInput, runBackendStepOutput);
|
||||
|
||||
if(runBackendStepOutput.getException() != null)
|
||||
{
|
||||
runProcessOutput.setException(runBackendStepOutput.getException());
|
||||
@ -409,8 +686,10 @@ public class RunProcessAction
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the list of steps which are eligible to run.
|
||||
**
|
||||
** lastStep will be included in the list, or not, based on includeLastStep.
|
||||
*******************************************************************************/
|
||||
private List<QStepMetaData> getAvailableStepList(ProcessState processState, QProcessMetaData process, String lastStep) throws QException
|
||||
static List<QStepMetaData> getAvailableStepList(ProcessState processState, QProcessMetaData process, String lastStep, boolean includeLastStep) throws QException
|
||||
{
|
||||
if(lastStep == null)
|
||||
{
|
||||
@ -437,6 +716,10 @@ public class RunProcessAction
|
||||
if(stepName.equals(lastStep))
|
||||
{
|
||||
foundLastStep = true;
|
||||
if(includeLastStep)
|
||||
{
|
||||
validStepNames.add(stepName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (stepNamesToSteps(process, validStepNames));
|
||||
@ -448,7 +731,7 @@ public class RunProcessAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<QStepMetaData> stepNamesToSteps(QProcessMetaData process, List<String> stepNames) throws QException
|
||||
private static List<QStepMetaData> stepNamesToSteps(QProcessMetaData process, List<String> stepNames) throws QException
|
||||
{
|
||||
List<QStepMetaData> result = new ArrayList<>();
|
||||
|
||||
@ -517,9 +800,13 @@ public class RunProcessAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected String determineBasepullKeyValue(QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
|
||||
protected String determineBasepullKeyValue(QProcessMetaData process, RunProcessInput runProcessInput, BasepullConfiguration basepullConfiguration) throws QException
|
||||
{
|
||||
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
|
||||
if(runProcessInput.getValueString(BASEPULL_KEY_VALUE) != null)
|
||||
{
|
||||
basepullKeyValue = runProcessInput.getValueString(BASEPULL_KEY_VALUE);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if process specifies that it uses variants, look for that data in the session and append to our basepull key //
|
||||
@ -528,13 +815,14 @@ public class RunProcessAction
|
||||
{
|
||||
QSession session = QContext.getQSession();
|
||||
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getVariantBackend());
|
||||
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue()))
|
||||
String variantTypeKey = backendMetaData.getBackendVariantsConfig().getVariantTypeKey();
|
||||
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(variantTypeKey))
|
||||
{
|
||||
LOG.warn("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
|
||||
}
|
||||
else
|
||||
{
|
||||
basepullKeyValue += "-" + session.getBackendVariants().get(backendMetaData.getVariantOptionsTableTypeValue());
|
||||
basepullKeyValue += "-" + session.getBackendVariants().get(variantTypeKey);
|
||||
}
|
||||
}
|
||||
|
||||
@ -551,7 +839,7 @@ public class RunProcessAction
|
||||
String basepullTableName = basepullConfiguration.getTableName();
|
||||
String basepullKeyFieldName = basepullConfiguration.getKeyField();
|
||||
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
|
||||
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration);
|
||||
String basepullKeyValue = determineBasepullKeyValue(process, runProcessInput, basepullConfiguration);
|
||||
|
||||
///////////////////////////////////////
|
||||
// get the stored basepull timestamp //
|
||||
@ -631,7 +919,7 @@ public class RunProcessAction
|
||||
String basepullKeyFieldName = basepullConfiguration.getKeyField();
|
||||
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
|
||||
Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp();
|
||||
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration);
|
||||
String basepullKeyValue = determineBasepullKeyValue(process, runProcessInput, basepullConfiguration);
|
||||
|
||||
///////////////////////////////////////
|
||||
// get the stored basepull timestamp //
|
||||
@ -663,4 +951,153 @@ public class RunProcessAction
|
||||
runProcessInput.getValues().put(BASEPULL_TIMESTAMP_FIELD, basepullConfiguration.getTimestampField());
|
||||
runProcessInput.getValues().put(BASEPULL_CONFIGURATION, basepullConfiguration);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void setupProcessTracer(RunProcessInput runProcessInput, QProcessMetaData process)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(process.getProcessTracerCodeReference() != null)
|
||||
{
|
||||
processTracer = QCodeLoader.getAdHoc(ProcessTracerInterface.class, process.getProcessTracerCodeReference());
|
||||
}
|
||||
|
||||
Serializable processTracerCodeReference = runProcessInput.getValue(PROCESS_TRACER_CODE_REFERENCE_FIELD);
|
||||
if(processTracerCodeReference != null)
|
||||
{
|
||||
if(processTracerCodeReference instanceof QCodeReference codeReference)
|
||||
{
|
||||
processTracer = QCodeLoader.getAdHoc(ProcessTracerInterface.class, codeReference);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error setting up processTracer", e, logPair("processName", runProcessInput.getProcessName()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void traceStartOrResume(RunProcessInput runProcessInput, QProcessMetaData process)
|
||||
{
|
||||
setupProcessTracer(runProcessInput, process);
|
||||
|
||||
try
|
||||
{
|
||||
if(processTracer != null)
|
||||
{
|
||||
if(StringUtils.hasContent(runProcessInput.getStartAfterStep()) || StringUtils.hasContent(runProcessInput.getStartAtStep()))
|
||||
{
|
||||
processTracer.handleProcessResume(runProcessInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
processTracer.handleProcessStart(runProcessInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.info("Error in traceStart", e, logPair("processName", runProcessInput.getProcessName()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void traceBreakOrFinish(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, Exception processException)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(processTracer != null)
|
||||
{
|
||||
ProcessState processState = runProcessOutput.getProcessState();
|
||||
boolean isBreak = true;
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// if there's no next step, that means the process is done //
|
||||
/////////////////////////////////////////////////////////////
|
||||
if(processState.getNextStepName().isEmpty())
|
||||
{
|
||||
isBreak = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// or if the next step is the last index, then we're also done //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
String nextStepName = processState.getNextStepName().get();
|
||||
int nextStepIndex = processState.getStepList().indexOf(nextStepName);
|
||||
if(nextStepIndex == processState.getStepList().size() - 1)
|
||||
{
|
||||
isBreak = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(isBreak)
|
||||
{
|
||||
processTracer.handleProcessBreak(runProcessInput, runProcessOutput, processException);
|
||||
}
|
||||
else
|
||||
{
|
||||
processTracer.handleProcessFinish(runProcessInput, runProcessOutput, processException);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.info("Error in traceProcessFinish", e, logPair("processName", runProcessInput.getProcessName()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void traceStepStart(RunBackendStepInput runBackendStepInput)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(processTracer != null)
|
||||
{
|
||||
processTracer.handleStepStart(runBackendStepInput);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.info("Error in traceStepFinish", e, logPair("processName", runBackendStepInput.getProcessName()), logPair("stepName", runBackendStepInput.getStepName()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void traceStepFinish(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(processTracer != null)
|
||||
{
|
||||
processTracer.handleStepFinish(runBackendStepInput, runBackendStepOutput);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.info("Error in traceStepFinish", e, logPair("processName", runBackendStepInput.getProcessName()), logPair("stepName", runBackendStepInput.getStepName()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ public class CsvExportStreamer implements ExportStreamerInterface
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error starting CSV report"));
|
||||
throw (new QReportingException("Error starting CSV report", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,14 @@ public interface ExportStreamerInterface
|
||||
// noop in base class
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
default void setExportStyleCustomizer(ExportStyleCustomizerInterface exportStyleCustomizer)
|
||||
{
|
||||
// noop in base class
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Called once per sheet, before any rows are available. Meant to write a
|
||||
** header, for example.
|
||||
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** interface for classes that can be used to customize visual style aspects of
|
||||
** exports/reports.
|
||||
**
|
||||
** Anticipates very different sub-interfaces based on the file type being generated,
|
||||
** and the capabilities of each. e.g., excel (bolds, fonts, cell merging) vs
|
||||
** json (different structure of objects).
|
||||
*******************************************************************************/
|
||||
public interface ExportStyleCustomizerInterface
|
||||
{
|
||||
}
|
@ -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.reporting;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||
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.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.Pair;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility for verifying that the ExportAction works for all tables, and all
|
||||
** exposed joins.
|
||||
**
|
||||
** Meant for use within a unit test, or maybe as part of an instance's boot-up/
|
||||
** validation.
|
||||
*******************************************************************************/
|
||||
public class ExportsFullInstanceVerifier
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ExportsFullInstanceVerifier.class);
|
||||
|
||||
private boolean filterForAtMostOneRowPerExport = true;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void verify(Collection<QTableMetaData> tables) throws QException
|
||||
{
|
||||
Map<Pair<String, String>, Exception> caughtExceptions = new LinkedHashMap<>();
|
||||
for(QTableMetaData table : tables)
|
||||
{
|
||||
if(table.isCapabilityEnabled(QContext.getQInstance().getBackendForTable(table.getName()), Capability.TABLE_QUERY))
|
||||
{
|
||||
LOG.info("Verifying Exports on table", logPair("tableName", table.getName()));
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// run the table by itself (no join fields) //
|
||||
//////////////////////////////////////////////
|
||||
runExport(table.getName(), Collections.emptyList(), "main-table-only", caughtExceptions);
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// run once w/ the fields from each exposed join //
|
||||
///////////////////////////////////////////////////
|
||||
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins()))
|
||||
{
|
||||
runExport(table.getName(), List.of(exposedJoin), "join-" + exposedJoin.getLabel(), caughtExceptions);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// run w/ all exposed joins (if there are any) //
|
||||
/////////////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(table.getExposedJoins()))
|
||||
{
|
||||
runExport(table.getName(), table.getExposedJoins(), "all-joins", caughtExceptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// log out an exceptions caught //
|
||||
//////////////////////////////////
|
||||
if(!caughtExceptions.isEmpty())
|
||||
{
|
||||
for(Map.Entry<Pair<String, String>, Exception> entry : caughtExceptions.entrySet())
|
||||
{
|
||||
LOG.info("Caught an exception verifying reports", entry.getValue(), logPair("tableName", entry.getKey().getA()), logPair("fieldName", entry.getKey().getB()));
|
||||
}
|
||||
throw (new QException("Reports Verification failed with " + caughtExceptions.size() + " exception" + StringUtils.plural(caughtExceptions.size())));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void runExport(String tableName, List<ExposedJoin> exposedJoinList, String description, Map<Pair<String, String>, Exception> caughtExceptions)
|
||||
{
|
||||
try
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// build the list of fieldNames to export - starting with all fields in the table //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
List<String> fieldNames = new ArrayList<>();
|
||||
for(QFieldMetaData field : QContext.getQInstance().getTable(tableName).getFields().values())
|
||||
{
|
||||
fieldNames.add(field.getName());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// add all fields from all exposed joins as well //
|
||||
///////////////////////////////////////////////////
|
||||
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(exposedJoinList))
|
||||
{
|
||||
QTableMetaData joinTable = QContext.getQInstance().getTable(exposedJoin.getJoinTable());
|
||||
for(QFieldMetaData field : joinTable.getFields().values())
|
||||
{
|
||||
fieldNames.add(joinTable.getName() + "." + field.getName());
|
||||
}
|
||||
}
|
||||
|
||||
LOG.info("Verifying export", logPair("description", description), logPair("fieldCount", fieldNames.size()));
|
||||
|
||||
QQueryFilter queryFilter = new QQueryFilter();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if caller is okay with a filter that should limit the report to a small number of rows (could be more than 1 for to-many joins), then do so //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(filterForAtMostOneRowPerExport)
|
||||
{
|
||||
queryFilter.withCriteria(QContext.getQInstance().getTable(tableName).getPrimaryKeyField(), QCriteriaOperator.EQUALS, 1);
|
||||
}
|
||||
|
||||
ExportInput exportInput = new ExportInput();
|
||||
exportInput.setTableName(tableName);
|
||||
exportInput.setFieldNames(fieldNames);
|
||||
exportInput.setReportDestination(new ReportDestination()
|
||||
.withReportOutputStream(new ByteArrayOutputStream())
|
||||
.withReportFormat(ReportFormat.CSV));
|
||||
exportInput.setQueryFilter(queryFilter);
|
||||
new ExportAction().execute(exportInput);
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
caughtExceptions.put(Pair.of(tableName, description), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for filterForAtMostOneRowPerExport
|
||||
*******************************************************************************/
|
||||
public boolean getFilterForAtMostOneRowPerExport()
|
||||
{
|
||||
return (this.filterForAtMostOneRowPerExport);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for filterForAtMostOneRowPerExport
|
||||
*******************************************************************************/
|
||||
public void setFilterForAtMostOneRowPerExport(boolean filterForAtMostOneRowPerExport)
|
||||
{
|
||||
this.filterForAtMostOneRowPerExport = filterForAtMostOneRowPerExport;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for filterForAtMostOneRowPerExport
|
||||
*******************************************************************************/
|
||||
public ExportsFullInstanceVerifier withFilterForAtMostOneRowPerExport(boolean filterForAtMostOneRowPerExport)
|
||||
{
|
||||
this.filterForAtMostOneRowPerExport = filterForAtMostOneRowPerExport;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -42,6 +42,7 @@ 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.ReportCustomRecordSourceInterface;
|
||||
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;
|
||||
@ -62,11 +63,15 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryHint;
|
||||
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.CriteriaMissingInputValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.FilterUseCase;
|
||||
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;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
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.FieldAndJoinTable;
|
||||
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.QReportDataSource;
|
||||
@ -158,6 +163,17 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
reportStreamer = reportFormat.newReportStreamer();
|
||||
}
|
||||
|
||||
if(reportInput.getExportStyleCustomizer() != null)
|
||||
{
|
||||
ExportStyleCustomizerInterface styleCustomizer = QCodeLoader.getAdHoc(ExportStyleCustomizerInterface.class, reportInput.getExportStyleCustomizer());
|
||||
reportStreamer.setExportStyleCustomizer(styleCustomizer);
|
||||
}
|
||||
else if(report.getExportStyleCustomizer() != null)
|
||||
{
|
||||
ExportStyleCustomizerInterface styleCustomizer = QCodeLoader.getAdHoc(ExportStyleCustomizerInterface.class, report.getExportStyleCustomizer());
|
||||
reportStreamer.setExportStyleCustomizer(styleCustomizer);
|
||||
}
|
||||
|
||||
reportStreamer.preRun(reportInput.getReportDestination(), views);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -206,7 +222,8 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(dataSourceTableView.getViewCustomizer() != null)
|
||||
{
|
||||
Function<QReportView, QReportView> viewCustomizerFunction = QCodeLoader.getFunction(dataSourceTableView.getViewCustomizer());
|
||||
@SuppressWarnings("unchecked")
|
||||
Function<QReportView, QReportView> viewCustomizerFunction = QCodeLoader.getAdHoc(Function.class, dataSourceTableView.getViewCustomizer());
|
||||
if(viewCustomizerFunction instanceof ReportViewCustomizer reportViewCustomizer)
|
||||
{
|
||||
reportViewCustomizer.setReportInput(reportInput);
|
||||
@ -301,10 +318,19 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
JoinsContext joinsContext = null;
|
||||
if(dataSource != null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// count records, if applicable, from the data source - for populating into the //
|
||||
// countByDataSource map, as well as for checking if too many rows (e.g., for excel) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
countDataSourceRecords(reportInput, dataSource, reportFormat);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there's a source table, set up a joins context, to use below for looking up fields //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
{
|
||||
joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
|
||||
countDataSourceRecords(reportInput, dataSource, reportFormat);
|
||||
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
|
||||
joinsContext = new JoinsContext(QContext.getQInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryFilter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,6 +354,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
field.setName(column.getName());
|
||||
if(StringUtils.hasContent(column.getLabel()))
|
||||
{
|
||||
|
||||
field.setLabel(column.getLabel());
|
||||
}
|
||||
fields.add(field);
|
||||
@ -345,23 +372,33 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
*******************************************************************************/
|
||||
private void countDataSourceRecords(ReportInput reportInput, QReportDataSource dataSource, ReportFormat reportFormat) throws QException
|
||||
{
|
||||
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
|
||||
setInputValuesInQueryFilter(reportInput, queryFilter);
|
||||
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(dataSource.getSourceTable());
|
||||
countInput.setFilter(queryFilter);
|
||||
countInput.setQueryJoins(dataSource.getQueryJoins());
|
||||
CountOutput countOutput = new CountAction().execute(countInput);
|
||||
|
||||
if(countOutput.getCount() != null)
|
||||
Integer count = null;
|
||||
if(dataSource.getCustomRecordSource() != null)
|
||||
{
|
||||
countByDataSource.put(dataSource.getName(), countOutput.getCount());
|
||||
// todo - add `count` method to interface?
|
||||
}
|
||||
else if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
{
|
||||
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
|
||||
setInputValuesInQueryFilter(reportInput, queryFilter);
|
||||
|
||||
if(reportFormat.getMaxRows() != null && countOutput.getCount() > reportFormat.getMaxRows())
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(dataSource.getSourceTable());
|
||||
countInput.setFilter(queryFilter);
|
||||
countInput.setQueryJoins(cloneDataSourceQueryJoins(dataSource));
|
||||
CountOutput countOutput = new CountAction().execute(countInput);
|
||||
|
||||
count = countOutput.getCount();
|
||||
}
|
||||
|
||||
if(count != null)
|
||||
{
|
||||
countByDataSource.put(dataSource.getName(), count);
|
||||
|
||||
if(reportFormat.getMaxRows() != null && count > reportFormat.getMaxRows())
|
||||
{
|
||||
throw (new QUserFacingException("The requested report would include more rows ("
|
||||
+ String.format("%,d", countOutput.getCount()) + ") than the maximum allowed ("
|
||||
+ String.format("%,d", count) + ") than the maximum allowed ("
|
||||
+ String.format("%,d", reportFormat.getMaxRows()) + ") for the selected file format (" + reportFormat + ")."));
|
||||
}
|
||||
}
|
||||
@ -369,6 +406,26 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static List<QueryJoin> cloneDataSourceQueryJoins(QReportDataSource dataSource)
|
||||
{
|
||||
if(dataSource == null || dataSource.getQueryJoins() == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
List<QueryJoin> rs = new ArrayList<>();
|
||||
for(QueryJoin queryJoin : dataSource.getQueryJoins())
|
||||
{
|
||||
rs.add(queryJoin.clone());
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -402,13 +459,19 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
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 //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// run a record pipe loop, over the query (or other data-supplier/source) for this data source //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
RecordPipe recordPipe = new BufferedRecordPipe(1000);
|
||||
new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) ->
|
||||
{
|
||||
if(dataSource.getSourceTable() != null)
|
||||
if(dataSource.getCustomRecordSource() != null)
|
||||
{
|
||||
ReportCustomRecordSourceInterface recordSource = QCodeLoader.getAdHoc(ReportCustomRecordSourceInterface.class, dataSource.getCustomRecordSource());
|
||||
recordSource.execute(reportInput, dataSource, recordPipe);
|
||||
return (true);
|
||||
}
|
||||
else if(dataSource.getSourceTable() != null)
|
||||
{
|
||||
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
|
||||
setInputValuesInQueryFilter(reportInput, queryFilter);
|
||||
@ -417,12 +480,12 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
queryInput.setRecordPipe(recordPipe);
|
||||
queryInput.setTableName(dataSource.getSourceTable());
|
||||
queryInput.setFilter(queryFilter);
|
||||
queryInput.setQueryJoins(dataSource.getQueryJoins());
|
||||
queryInput.setQueryJoins(cloneDataSourceQueryJoins(dataSource));
|
||||
queryInput.withQueryHint(QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
|
||||
queryInput.withQueryHint(QueryHint.MAY_USE_READ_ONLY_BACKEND);
|
||||
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource, new JoinsContext(reportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryInput.getFilter())));
|
||||
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource));
|
||||
|
||||
if(dataSource.getQueryInputCustomizer() != null)
|
||||
{
|
||||
@ -474,7 +537,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
}
|
||||
consumedCount.getAndAdd(records.size());
|
||||
|
||||
return (consumeRecords(reportInput, dataSource, records, tableView, summaryViews, variantViews));
|
||||
return (consumeRecords(dataSource, records, tableView, summaryViews, variantViews));
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////
|
||||
@ -493,7 +556,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext) throws QException
|
||||
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource) throws QException
|
||||
{
|
||||
Set<String> fieldsToTranslatePossibleValues = new HashSet<>();
|
||||
|
||||
@ -517,7 +580,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
// all pivotFields that are possible value sources are implicitly translated //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData mainTable = QContext.getQInstance().getTable(dataSource.getSourceTable());
|
||||
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(mainTable, summaryFieldName);
|
||||
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(mainTable, summaryFieldName);
|
||||
if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
|
||||
{
|
||||
fieldsToTranslatePossibleValues.add(summaryFieldName);
|
||||
@ -530,32 +593,6 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException
|
||||
{
|
||||
if(fieldName.indexOf('.') > -1)
|
||||
{
|
||||
String joinTableName = fieldName.replaceAll("\\..*", "");
|
||||
String joinFieldName = fieldName.replaceAll(".*\\.", "");
|
||||
|
||||
QTableMetaData joinTable = QContext.getQInstance().getTable(joinTableName);
|
||||
if(joinTable == null)
|
||||
{
|
||||
throw (new QException("Unrecognized join table name: " + joinTableName));
|
||||
}
|
||||
|
||||
return new FieldAndJoinTable(joinTable.getField(joinFieldName), joinTable);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new FieldAndJoinTable(mainTable.getField(fieldName), mainTable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -566,7 +603,56 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
return;
|
||||
}
|
||||
|
||||
queryFilter.interpretValues(reportInput.getInputValues());
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for reports defined in meta-data, the established rule is, that missing input variable values are discarded. //
|
||||
// but for non-meta-data reports (e.g., user-saved), we expect an exception for missing values. //
|
||||
// so, set those use-cases up. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
FilterUseCase filterUseCase;
|
||||
if(StringUtils.hasContent(reportInput.getReportName()) && QContext.getQInstance().getReport(reportInput.getReportName()) != null)
|
||||
{
|
||||
filterUseCase = new ReportFromMetaDataFilterUseCase();
|
||||
}
|
||||
else
|
||||
{
|
||||
filterUseCase = new ReportNotFromMetaDataFilterUseCase();
|
||||
}
|
||||
|
||||
queryFilter.interpretValues(reportInput.getInputValues(), filterUseCase);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static class ReportFromMetaDataFilterUseCase implements FilterUseCase
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public CriteriaMissingInputValueBehavior getDefaultCriteriaMissingInputValueBehavior()
|
||||
{
|
||||
return CriteriaMissingInputValueBehavior.REMOVE_FROM_FILTER;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static class ReportNotFromMetaDataFilterUseCase implements FilterUseCase
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public CriteriaMissingInputValueBehavior getDefaultCriteriaMissingInputValueBehavior()
|
||||
{
|
||||
return CriteriaMissingInputValueBehavior.THROW_EXCEPTION;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -574,9 +660,9 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Integer consumeRecords(ReportInput reportInput, QReportDataSource dataSource, List<QRecord> records, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
|
||||
private Integer consumeRecords(QReportDataSource dataSource, List<QRecord> records, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
|
||||
{
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
QTableMetaData table = QContext.getQInstance().getTable(dataSource.getSourceTable());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// if this record goes on a table view, add it to the report streamer now //
|
||||
@ -586,7 +672,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if any fields are 'showPossibleValueLabel', then move display values for them into the record's values map //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QReportField column : tableView.getColumns())
|
||||
for(QReportField column : CollectionUtils.nonNullList(tableView.getColumns()))
|
||||
{
|
||||
if(column.getShowPossibleValueLabel())
|
||||
{
|
||||
@ -657,7 +743,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
SummaryKey key = new SummaryKey();
|
||||
for(String summaryFieldName : view.getSummaryFields())
|
||||
{
|
||||
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName);
|
||||
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, summaryFieldName);
|
||||
Serializable summaryValue = record.getValue(summaryFieldName);
|
||||
if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
|
||||
{
|
||||
@ -687,7 +773,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> viewAggregates, SummaryKey key) throws QException
|
||||
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> viewAggregates, SummaryKey key)
|
||||
{
|
||||
Map<String, AggregatesInterface<?, ?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
|
||||
addRecordToAggregatesMap(table, record, keyAggregates);
|
||||
@ -698,7 +784,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?, ?>> aggregatesMap) throws QException
|
||||
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?, ?>> aggregatesMap)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - an optimization could be, to only compute aggregates that we'll need... //
|
||||
@ -706,13 +792,13 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
for(String fieldName : record.getValues().keySet())
|
||||
{
|
||||
QFieldMetaData field = null;
|
||||
QFieldMetaData field;
|
||||
try
|
||||
{
|
||||
//////////////////////////////////////////////////////
|
||||
// todo - memoize this, if we ever need to optimize //
|
||||
//////////////////////////////////////////////////////
|
||||
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, fieldName);
|
||||
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, fieldName);
|
||||
field = fieldAndJoinTable.field();
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -779,9 +865,14 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
List<QReportView> reportViews = views.stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
|
||||
for(QReportView view : reportViews)
|
||||
{
|
||||
QReportDataSource dataSource = getDataSource(view.getDataSourceName());
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
|
||||
QReportDataSource dataSource = getDataSource(view.getDataSourceName());
|
||||
if(dataSource == null)
|
||||
{
|
||||
throw new QReportingException("Data source for summary view was not found (viewName=" + view.getName() + ", dataSourceName=" + view.getDataSourceName() + ").");
|
||||
}
|
||||
|
||||
QTableMetaData table = QContext.getQInstance().getTable(dataSource.getSourceTable());
|
||||
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
|
||||
|
||||
ExportInput exportInput = new ExportInput();
|
||||
exportInput.setReportDestination(reportInput.getReportDestination());
|
||||
@ -852,7 +943,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
List<QFieldMetaData> fields = new ArrayList<>();
|
||||
for(String summaryFieldName : view.getSummaryFields())
|
||||
{
|
||||
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName);
|
||||
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(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())
|
||||
@ -867,9 +958,8 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private SummaryOutput computeSummaryRowsForView(ReportInput reportInput, QReportView view, QTableMetaData table) throws QReportingException, QFormulaException
|
||||
private SummaryOutput computeSummaryRowsForView(ReportInput reportInput, QReportView view, QTableMetaData table) throws QFormulaException
|
||||
{
|
||||
QValueFormatter valueFormatter = new QValueFormatter();
|
||||
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
|
||||
variableInterpreter.addValueMap("input", reportInput.getInputValues());
|
||||
variableInterpreter.addValueMap("total", getSummaryValuesForInterpreter(totalAggregates));
|
||||
@ -941,10 +1031,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(view.getOrderByFields()))
|
||||
{
|
||||
summaryRows.sort((o1, o2) ->
|
||||
{
|
||||
return summaryRowComparator(view, o1, o2);
|
||||
});
|
||||
summaryRows.sort((o1, o2) -> summaryRowComparator(view, o1, o2));
|
||||
}
|
||||
|
||||
////////////////
|
||||
@ -979,8 +1066,6 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
Serializable serializable = getValueForColumn(variableInterpreter, column);
|
||||
totalRow.setValue(column.getName(), serializable);
|
||||
thisRowValues.put(column.getName(), serializable);
|
||||
|
||||
String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1003,7 +1088,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
titleValues.add(variableInterpreter.interpret(titleField));
|
||||
}
|
||||
|
||||
title = new QValueFormatter().formatStringWithValues(view.getTitleFormat(), titleValues);
|
||||
title = QValueFormatter.formatStringWithValues(view.getTitleFormat(), titleValues);
|
||||
}
|
||||
else if(StringUtils.hasContent(view.getTitleFormat()))
|
||||
{
|
||||
@ -1110,27 +1195,4 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable)
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getLabel(QTableMetaData mainTable)
|
||||
{
|
||||
if(mainTable.getName().equals(joinTable.getName()))
|
||||
{
|
||||
return (field.getLabel());
|
||||
}
|
||||
else
|
||||
{
|
||||
return (joinTable.getLabel() + ": " + field.getLabel());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.customizers;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Interface to be implemented to do a custom source of data for a report
|
||||
** (instead of just a query against a table).
|
||||
*******************************************************************************/
|
||||
public interface ReportCustomRecordSourceInterface
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
** Given the report input, put records into the pipe, for the report.
|
||||
***************************************************************************/
|
||||
void execute(ReportInput reportInput, QReportDataSource reportDataSource, RecordPipe recordPipe) throws QException;
|
||||
|
||||
}
|
@ -124,7 +124,7 @@ public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
|
||||
if(workbook == null)
|
||||
{
|
||||
String appName = ObjectUtils.tryAndRequireNonNullElse(() -> QContext.getQInstance().getBranding().getAppName(), "QQQ");
|
||||
QInstance instance = exportInput.getInstance();
|
||||
QInstance instance = QContext.getQInstance();
|
||||
if(instance != null && instance.getBranding() != null && instance.getBranding().getCompanyName() != null)
|
||||
{
|
||||
appName = instance.getBranding().getCompanyName();
|
||||
|
@ -46,6 +46,7 @@ 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.ExportStyleCustomizerInterface;
|
||||
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;
|
||||
@ -77,6 +78,7 @@ 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;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -112,9 +114,10 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
public static final String EXCEL_DATE_FORMAT = "yyyy-MM-dd";
|
||||
public static final String EXCEL_DATE_TIME_FORMAT = "yyyy-MM-dd H:mm:ss";
|
||||
|
||||
private PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface();
|
||||
private ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface;
|
||||
|
||||
private Map<String, String> excelCellFormats;
|
||||
private Map<String, XSSFCellStyle> styles = new HashMap<>();
|
||||
private Map<String, XSSFCellStyle> styles = new HashMap<>();
|
||||
|
||||
private int rowNo = 0;
|
||||
private int sheetIndex = 1;
|
||||
@ -124,10 +127,11 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
private Writer activeSheetWriter = null;
|
||||
private StreamedSheetWriter sheetWriter = null;
|
||||
|
||||
private QReportView currentView = null;
|
||||
private Map<String, List<QFieldMetaData>> fieldsPerView = new HashMap<>();
|
||||
private Map<String, Integer> rowsPerView = new HashMap<>();
|
||||
private Map<String, String> labelViewsByName = new HashMap<>();
|
||||
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<>();
|
||||
private Map<String, String> sheetReferenceByViewName = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
@ -180,6 +184,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
String sheetReference = sheet.getPackagePart().getPartName().getName().substring(1);
|
||||
sheetMapByExcelReference.put(sheetReference, sheet);
|
||||
sheetMapByViewName.put(view.getName(), sheet);
|
||||
sheetReferenceByViewName.put(view.getName(), sheetReference);
|
||||
sheetCounter++;
|
||||
}
|
||||
|
||||
@ -400,6 +405,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
dateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT));
|
||||
styles.put("datetime", dateTimeStyle);
|
||||
|
||||
PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface();
|
||||
styles.put("title", poiExcelStylerInterface.createStyleForTitle(workbook, createHelper));
|
||||
styles.put("header", poiExcelStylerInterface.createStyleForHeader(workbook, createHelper));
|
||||
styles.put("footer", poiExcelStylerInterface.createStyleForFooter(workbook, createHelper));
|
||||
@ -411,6 +417,11 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
XSSFCellStyle footerDateTimeStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper);
|
||||
footerDateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT));
|
||||
styles.put("footer-datetime", footerDateTimeStyle);
|
||||
|
||||
if(styleCustomizerInterface != null)
|
||||
{
|
||||
styleCustomizerInterface.customizeStyles(styles, workbook, createHelper);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -446,7 +457,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
// - with a new output stream writer //
|
||||
// - and with a SpreadsheetWriter //
|
||||
//////////////////////////////////////////
|
||||
zipOutputStream.putNextEntry(new ZipEntry("xl/worksheets/sheet" + this.sheetIndex++ + ".xml"));
|
||||
zipOutputStream.putNextEntry(new ZipEntry(sheetReferenceByViewName.get(view.getName())));
|
||||
activeSheetWriter = new OutputStreamWriter(zipOutputStream);
|
||||
sheetWriter = new StreamedSheetWriter(activeSheetWriter);
|
||||
|
||||
@ -456,7 +467,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
}
|
||||
else
|
||||
{
|
||||
sheetWriter.beginSheet();
|
||||
sheetWriter.beginSheet(view, styleCustomizerInterface);
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// put the title and header rows in the sheet //
|
||||
@ -558,6 +569,16 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static void setStyleForField(QRecord record, String fieldName, String styleName)
|
||||
{
|
||||
record.setDisplayValue(fieldName + ":excelStyle", styleName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -565,12 +586,12 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
{
|
||||
sheetWriter.insertRow(rowNo++);
|
||||
|
||||
int styleIndex = -1;
|
||||
int baseStyleIndex = -1;
|
||||
int dateStyleIndex = styles.get("date").getIndex();
|
||||
int dateTimeStyleIndex = styles.get("datetime").getIndex();
|
||||
if(isFooter)
|
||||
{
|
||||
styleIndex = styles.get("footer").getIndex();
|
||||
baseStyleIndex = styles.get("footer").getIndex();
|
||||
dateStyleIndex = styles.get("footer-date").getIndex();
|
||||
dateTimeStyleIndex = styles.get("footer-datetime").getIndex();
|
||||
}
|
||||
@ -580,6 +601,13 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
{
|
||||
Serializable value = qRecord.getValue(field.getName());
|
||||
|
||||
String overrideStyleName = qRecord.getDisplayValue(field.getName() + ":excelStyle");
|
||||
int styleIndex = baseStyleIndex;
|
||||
if(overrideStyleName != null)
|
||||
{
|
||||
styleIndex = styles.get(overrideStyleName).getIndex();
|
||||
}
|
||||
|
||||
if(value != null)
|
||||
{
|
||||
if(value instanceof String s)
|
||||
@ -704,7 +732,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
{
|
||||
if(!ReportType.PIVOT.equals(currentView.getType()))
|
||||
{
|
||||
sheetWriter.endSheet();
|
||||
sheetWriter.endSheet(currentView, styleCustomizerInterface);
|
||||
}
|
||||
|
||||
activeSheetWriter.flush();
|
||||
@ -813,7 +841,29 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
|
||||
*******************************************************************************/
|
||||
protected PoiExcelStylerInterface getStylerInterface()
|
||||
{
|
||||
if(styleCustomizerInterface != null)
|
||||
{
|
||||
return styleCustomizerInterface.getExcelStyler();
|
||||
}
|
||||
|
||||
return (new PlainPoiExcelStyler());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void setExportStyleCustomizer(ExportStyleCustomizerInterface exportStyleCustomizer)
|
||||
{
|
||||
if(exportStyleCustomizer instanceof ExcelPoiBasedStreamingStyleCustomizerInterface poiExcelStylerInterface)
|
||||
{
|
||||
this.styleCustomizerInterface = poiExcelStylerInterface;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.debug("Supplied export style customizer is not an instance of ExcelPoiStyleCustomizerInterface, so will not be used for an excel export", logPair("exportStyleCustomizerClass", exportStyleCustomizer.getClass().getSimpleName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStyleCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
import org.apache.poi.ss.usermodel.CreationHelper;
|
||||
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** style customization points for Excel files generated via our streaming POI.
|
||||
*******************************************************************************/
|
||||
public interface ExcelPoiBasedStreamingStyleCustomizerInterface extends ExportStyleCustomizerInterface
|
||||
{
|
||||
/***************************************************************************
|
||||
** slightly legacy way we did excel styles - but get an instance of object
|
||||
** that defaults "default" styles (header, footer, etc).
|
||||
***************************************************************************/
|
||||
default PoiExcelStylerInterface getExcelStyler()
|
||||
{
|
||||
return (new PlainPoiExcelStyler());
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** either change "default" styles put in the styles map, or create new ones
|
||||
** which can then be applied to row/field values (cells) via:
|
||||
** ExcelPoiBasedStreamingExportStreamer.setStyleForField(row, fieldName, styleName);
|
||||
***************************************************************************/
|
||||
default void customizeStyles(Map<String, XSSFCellStyle> styles, XSSFWorkbook workbook, CreationHelper createHelper)
|
||||
{
|
||||
//////////////////
|
||||
// noop default //
|
||||
//////////////////
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** for a given view (sheet), return a list of custom column widths.
|
||||
** any nulls in the list are ignored (so default width is used).
|
||||
***************************************************************************/
|
||||
default List<Integer> getColumnWidthsForView(QReportView view)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** for a given view (sheet), return a list of any ranges which should be
|
||||
** merged, as in "A1:C1" (first three cells in first row).
|
||||
***************************************************************************/
|
||||
default List<String> getMergedRangesForView(QReportView view)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
}
|
@ -25,7 +25,10 @@ package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import org.apache.poi.ss.util.CellReference;
|
||||
|
||||
|
||||
@ -53,13 +56,33 @@ public class StreamedSheetWriter
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void beginSheet() throws IOException
|
||||
public void beginSheet(QReportView view, ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface) throws IOException
|
||||
{
|
||||
writer.write("""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<sheetData>""");
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">""");
|
||||
|
||||
if(styleCustomizerInterface != null && view != null)
|
||||
{
|
||||
List<Integer> columnWidths = styleCustomizerInterface.getColumnWidthsForView(view);
|
||||
if(CollectionUtils.nullSafeHasContents(columnWidths))
|
||||
{
|
||||
writer.write("<cols>");
|
||||
for(int i = 0; i < columnWidths.size(); i++)
|
||||
{
|
||||
Integer width = columnWidths.get(i);
|
||||
if(width != null)
|
||||
{
|
||||
writer.write("""
|
||||
<col min="%d" max="%d" width="%d" customWidth="1"/>
|
||||
""".formatted(i + 1, i + 1, width));
|
||||
}
|
||||
}
|
||||
writer.write("</cols>");
|
||||
}
|
||||
}
|
||||
|
||||
writer.write("<sheetData>");
|
||||
}
|
||||
|
||||
|
||||
@ -67,11 +90,25 @@ public class StreamedSheetWriter
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void endSheet() throws IOException
|
||||
public void endSheet(QReportView view, ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface) throws IOException
|
||||
{
|
||||
writer.write("""
|
||||
</sheetData>
|
||||
</worksheet>""");
|
||||
writer.write("</sheetData>");
|
||||
|
||||
if(styleCustomizerInterface != null && view != null)
|
||||
{
|
||||
List<String> mergedRanges = styleCustomizerInterface.getMergedRangesForView(view);
|
||||
if(CollectionUtils.nullSafeHasContents(mergedRanges))
|
||||
{
|
||||
writer.write(String.format("<mergeCells count=\"%d\">", mergedRanges.size()));
|
||||
for(String range : mergedRanges)
|
||||
{
|
||||
writer.write(String.format("<mergeCell ref=\"%s\"/>", range));
|
||||
}
|
||||
writer.write("</mergeCells>");
|
||||
}
|
||||
}
|
||||
|
||||
writer.write("</worksheet>");
|
||||
}
|
||||
|
||||
|
||||
@ -151,7 +188,7 @@ public class StreamedSheetWriter
|
||||
{
|
||||
rs.append(""");
|
||||
}
|
||||
else if (c < 32 && c != '\t' && c != '\n')
|
||||
else if(c < 32 && c != '\t' && c != '\n')
|
||||
{
|
||||
rs.append(' ');
|
||||
}
|
||||
@ -161,7 +198,7 @@ public class StreamedSheetWriter
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Integer> m = new HashMap();
|
||||
Map<String, Integer> m = new HashMap<>();
|
||||
m.computeIfAbsent("s", (s) -> 3);
|
||||
|
||||
value = rs.toString();
|
||||
|
@ -53,7 +53,8 @@ public class QJavaExecutor implements QCodeExecutor
|
||||
Serializable output;
|
||||
try
|
||||
{
|
||||
Function<Map<String, Object>, Serializable> function = QCodeLoader.getFunction(codeReference);
|
||||
@SuppressWarnings("unchecked")
|
||||
Function<Map<String, Object>, Serializable> function = QCodeLoader.getAdHoc(Function.class, codeReference);
|
||||
output = function.apply(context);
|
||||
}
|
||||
catch(Exception e)
|
||||
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger;
|
||||
@ -97,7 +98,7 @@ public class RecordScriptTestInterface implements TestScriptActionInterface
|
||||
}
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(new QueryInput(tableName)
|
||||
.withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordPrimaryKeyList.split(","))))
|
||||
.withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, Arrays.stream(recordPrimaryKeyList.split(",")).toList())))
|
||||
.withIncludeAssociations(true));
|
||||
if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords()))
|
||||
{
|
||||
|
@ -154,8 +154,9 @@ public class RunAdHocRecordScriptAction
|
||||
Method qRecordListToApiRecordList = apiScriptUtilsClass.getMethod("qRecordListToApiRecordList", List.class, String.class, String.class, String.class);
|
||||
Object apiRecordList = qRecordListToApiRecordList.invoke(null, input.getRecordList(), input.getTableName(), scriptRevision.getApiName(), scriptRevision.getApiVersion());
|
||||
|
||||
// noinspection unchecked
|
||||
return (ArrayList<? extends Serializable>) apiRecordList;
|
||||
@SuppressWarnings("unchecked")
|
||||
ArrayList<? extends Serializable> rs = (ArrayList<? extends Serializable>) apiRecordList;
|
||||
return rs;
|
||||
}
|
||||
catch(ClassNotFoundException e)
|
||||
{
|
||||
|
@ -32,6 +32,7 @@ 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.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
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.model.querystats.QueryStat;
|
||||
@ -82,6 +83,22 @@ public class CountAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** shorthand way to call for the most common use-case, when you just want the
|
||||
** count to be returned, and you just want to pass in a table name and filter.
|
||||
*******************************************************************************/
|
||||
public static Integer execute(String tableName, QQueryFilter filter) throws QException
|
||||
{
|
||||
CountAction countAction = new CountAction();
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(tableName);
|
||||
countInput.setFilter(filter);
|
||||
CountOutput countOutput = countAction.execute(countInput);
|
||||
return (countOutput.getCount());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -82,6 +82,11 @@ public class DeleteAction
|
||||
{
|
||||
ActionHelper.validateSession(deleteInput);
|
||||
|
||||
if(deleteInput.getTableName() == null)
|
||||
{
|
||||
throw (new QException("Table name was not specified in delete input"));
|
||||
}
|
||||
|
||||
QTableMetaData table = deleteInput.getTable();
|
||||
String primaryKeyFieldName = table.getPrimaryKeyField();
|
||||
QFieldMetaData primaryKeyField = table.getField(primaryKeyFieldName);
|
||||
@ -320,7 +325,7 @@ public class DeleteAction
|
||||
QTableMetaData table = deleteInput.getTable();
|
||||
List<QRecord> primaryKeysNotFound = validateRecordsExistAndCanBeAccessed(deleteInput, oldRecordList.get());
|
||||
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, oldRecordList.get(), ValidateRecordSecurityLockHelper.Action.DELETE);
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, oldRecordList.get(), ValidateRecordSecurityLockHelper.Action.DELETE, deleteInput.getTransaction());
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// after all validations, run the pre-delete customizer, if there is one //
|
||||
|
@ -238,6 +238,11 @@ public class GetAction
|
||||
*******************************************************************************/
|
||||
public static QRecord execute(String tableName, Serializable primaryKey) throws QException
|
||||
{
|
||||
if(primaryKey instanceof QQueryFilter)
|
||||
{
|
||||
LOG.warn("Unexpected use of QQueryFilter instead of primary key in GetAction call");
|
||||
}
|
||||
|
||||
GetAction getAction = new GetAction();
|
||||
GetInput getInput = new GetInput(tableName).withPrimaryKey(primaryKey);
|
||||
return getAction.executeForRecord(getInput);
|
||||
@ -352,7 +357,7 @@ public class GetAction
|
||||
{
|
||||
if(qPossibleValueTranslator == null)
|
||||
{
|
||||
qPossibleValueTranslator = new QPossibleValueTranslator(getInput.getInstance(), getInput.getSession());
|
||||
qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
|
||||
}
|
||||
qPossibleValueTranslator.translatePossibleValuesInRecords(getInput.getTable(), List.of(returnRecord));
|
||||
}
|
||||
|
@ -67,6 +67,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
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 static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
@ -110,6 +111,12 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
public InsertOutput execute(InsertInput insertInput) throws QException
|
||||
{
|
||||
ActionHelper.validateSession(insertInput);
|
||||
|
||||
if(!StringUtils.hasContent(insertInput.getTableName()))
|
||||
{
|
||||
throw (new QException("Table name was not specified in insert input"));
|
||||
}
|
||||
|
||||
QTableMetaData table = insertInput.getTable();
|
||||
|
||||
if(table == null)
|
||||
@ -122,7 +129,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
/////////////////////////////
|
||||
// run standard validators //
|
||||
/////////////////////////////
|
||||
performValidations(insertInput, false);
|
||||
performValidations(insertInput, false, false);
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// use the backend module to actually do the insert //
|
||||
@ -225,23 +232,26 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void performValidations(InsertInput insertInput, boolean isPreview) throws QException
|
||||
public void performValidations(InsertInput insertInput, boolean isPreview, boolean didAlreadyRunCustomizer) throws QException
|
||||
{
|
||||
if(CollectionUtils.nullSafeIsEmpty(insertInput.getRecords()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QTableMetaData table = insertInput.getTable();
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// load the pre-insert customizer and set it up, if there is one //
|
||||
// then we'll run it based on its WhenToRun value //
|
||||
// note - if we already ran it, then don't re-run it! //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
Optional<TableCustomizerInterface> preInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_INSERT_RECORD.getRole());
|
||||
if(preInsertCustomizer.isPresent())
|
||||
{
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS);
|
||||
}
|
||||
Optional<TableCustomizerInterface> preInsertCustomizer = didAlreadyRunCustomizer ? Optional.empty() : QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_INSERT_RECORD.getRole());
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS);
|
||||
|
||||
setDefaultValuesInRecords(table, insertInput.getRecords());
|
||||
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, insertInput.getInstance(), table, insertInput.getRecords(), null);
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, QContext.getQInstance(), table, insertInput.getRecords(), null);
|
||||
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_UNIQUE_KEY_CHECKS);
|
||||
setErrorsIfUniqueKeyErrors(insertInput, table);
|
||||
@ -253,7 +263,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
}
|
||||
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_SECURITY_CHECKS);
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT);
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT, insertInput.getTransaction());
|
||||
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.AFTER_ALL_VALIDATIONS);
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -50,8 +51,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperat
|
||||
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.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
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.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
@ -64,6 +67,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.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
@ -101,6 +105,8 @@ public class QueryAction
|
||||
throw (new QException("A table named [" + queryInput.getTableName() + "] was not found in the active QInstance"));
|
||||
}
|
||||
|
||||
validateFieldNamesToInclude(queryInput);
|
||||
|
||||
QBackendMetaData backend = queryInput.getBackend();
|
||||
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_QUERY_RECORD.getRole());
|
||||
this.queryInput = queryInput;
|
||||
@ -158,6 +164,125 @@ public class QueryAction
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** if QueryInput contains a set of FieldNamesToInclude, then validate that
|
||||
** those are known field names in the table being queried, or a selected
|
||||
** queryJoin.
|
||||
***************************************************************************/
|
||||
static void validateFieldNamesToInclude(QueryInput queryInput) throws QException
|
||||
{
|
||||
Set<String> fieldNamesToInclude = queryInput.getFieldNamesToInclude();
|
||||
if(fieldNamesToInclude == null)
|
||||
{
|
||||
////////////////////////////////
|
||||
// null set means select all. //
|
||||
////////////////////////////////
|
||||
return;
|
||||
}
|
||||
|
||||
if(fieldNamesToInclude.isEmpty())
|
||||
{
|
||||
/////////////////////////////////////
|
||||
// empty set, however, is an error //
|
||||
/////////////////////////////////////
|
||||
throw (new QException("An empty set of fieldNamesToInclude was given as queryInput, which is not allowed."));
|
||||
}
|
||||
|
||||
List<String> unrecognizedFieldNames = new ArrayList<>();
|
||||
Map<String, QTableMetaData> selectedQueryJoins = null;
|
||||
for(String fieldName : fieldNamesToInclude)
|
||||
{
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
////////////////////////////////////////////////
|
||||
// handle names with dots - fields from joins //
|
||||
////////////////////////////////////////////////
|
||||
String[] parts = fieldName.split("\\.");
|
||||
if(parts.length != 2)
|
||||
{
|
||||
unrecognizedFieldNames.add(fieldName);
|
||||
}
|
||||
else
|
||||
{
|
||||
String tableOrAlias = parts[0];
|
||||
String fieldNamePart = parts[1];
|
||||
|
||||
////////////////////////////////////////////
|
||||
// build map of queryJoins being selected //
|
||||
////////////////////////////////////////////
|
||||
if(selectedQueryJoins == null)
|
||||
{
|
||||
selectedQueryJoins = new HashMap<>();
|
||||
for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryInput.getQueryJoins()))
|
||||
{
|
||||
if(queryJoin.getSelect())
|
||||
{
|
||||
String joinTableOrAlias = queryJoin.getJoinTableOrItsAlias();
|
||||
QTableMetaData joinTable = QContext.getQInstance().getTable(queryJoin.getJoinTable());
|
||||
if(joinTable != null)
|
||||
{
|
||||
selectedQueryJoins.put(joinTableOrAlias, joinTable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!selectedQueryJoins.containsKey(tableOrAlias))
|
||||
{
|
||||
///////////////////////////////////////////
|
||||
// unrecognized tableOrAlias is an error //
|
||||
///////////////////////////////////////////
|
||||
unrecognizedFieldNames.add(fieldName);
|
||||
}
|
||||
else
|
||||
{
|
||||
QTableMetaData joinTable = selectedQueryJoins.get(tableOrAlias);
|
||||
if(!joinTable.getFields().containsKey(fieldNamePart))
|
||||
{
|
||||
//////////////////////////////////////////////////////////
|
||||
// unrecognized field within the join table is an error //
|
||||
//////////////////////////////////////////////////////////
|
||||
unrecognizedFieldNames.add(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// non-join fields - just ensure field name is in table's fields map //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
if(!queryInput.getTable().getFields().containsKey(fieldName))
|
||||
{
|
||||
unrecognizedFieldNames.add(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!unrecognizedFieldNames.isEmpty())
|
||||
{
|
||||
throw (new QException("QueryInput contained " + unrecognizedFieldNames.size() + " unrecognized field name" + StringUtils.plural(unrecognizedFieldNames) + ": " + StringUtils.join(",", unrecognizedFieldNames)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** shorthand way to call for the most common use-case, when you just want the
|
||||
** entities to be returned, and you just want to pass in a table name and filter.
|
||||
*******************************************************************************/
|
||||
public static <T extends QRecordEntity> List<T> execute(String tableName, Class<T> entityClass, QQueryFilter filter) throws QException
|
||||
{
|
||||
QueryAction queryAction = new QueryAction();
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(tableName);
|
||||
queryInput.setFilter(filter);
|
||||
QueryOutput queryOutput = queryAction.execute(queryInput);
|
||||
return (queryOutput.getRecordEntities(entityClass));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** shorthand way to call for the most common use-case, when you just want the
|
||||
** records to be returned, and you just want to pass in a table name and filter.
|
||||
@ -297,7 +422,7 @@ public class QueryAction
|
||||
{
|
||||
if(qPossibleValueTranslator == null)
|
||||
{
|
||||
qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession());
|
||||
qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
|
||||
}
|
||||
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records, queryInput.getQueryJoins(), queryInput.getFieldsToTranslatePossibleValues());
|
||||
}
|
||||
|
@ -47,7 +47,8 @@ public class StorageAction
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** create an output stream in the storage backend - that can be written to,
|
||||
** for the purpose of inserting or writing a file into storage.
|
||||
*******************************************************************************/
|
||||
public OutputStream createOutputStream(StorageInput storageInput) throws QException
|
||||
{
|
||||
@ -59,7 +60,8 @@ public class StorageAction
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** create an input stream in the storage backend - that can be read from,
|
||||
** for the purpose of getting or reading a file from storage.
|
||||
*******************************************************************************/
|
||||
public InputStream getInputStream(StorageInput storageInput) throws QException
|
||||
{
|
||||
|
@ -74,6 +74,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
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.commons.lang.BooleanUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
@ -118,6 +119,11 @@ public class UpdateAction
|
||||
{
|
||||
ActionHelper.validateSession(updateInput);
|
||||
|
||||
if(!StringUtils.hasContent(updateInput.getTableName()))
|
||||
{
|
||||
throw (new QException("Table name was not specified in update input"));
|
||||
}
|
||||
|
||||
QTableMetaData table = updateInput.getTable();
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
@ -252,7 +258,7 @@ public class UpdateAction
|
||||
behaviorsToOmit = Set.of(DynamicDefaultValueBehavior.MODIFY_DATE);
|
||||
}
|
||||
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, updateInput.getInstance(), table, updateInput.getRecords(), behaviorsToOmit);
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, QContext.getQInstance(), table, updateInput.getRecords(), behaviorsToOmit);
|
||||
validatePrimaryKeysAreGiven(updateInput);
|
||||
|
||||
if(oldRecordList.isPresent())
|
||||
@ -261,7 +267,7 @@ public class UpdateAction
|
||||
}
|
||||
else
|
||||
{
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE, updateInput.getTransaction());
|
||||
}
|
||||
|
||||
if(updateInput.getInputSource().shouldValidateRequiredFields())
|
||||
@ -374,7 +380,7 @@ public class UpdateAction
|
||||
}
|
||||
}
|
||||
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE, updateInput.getTransaction());
|
||||
|
||||
for(QRecord record : page)
|
||||
{
|
||||
|
@ -31,16 +31,12 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
@ -54,12 +50,10 @@ import com.kingsrook.qqq.backend.core.model.querystats.QueryStatCriteriaField;
|
||||
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatJoinTable;
|
||||
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatOrderByField;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
|
||||
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.model.tables.QQQTableTableManager;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -371,7 +365,7 @@ public class QueryStatManager
|
||||
//////////////////////
|
||||
// set the table id //
|
||||
//////////////////////
|
||||
Integer qqqTableId = getQQQTableId(queryStat.getTableName());
|
||||
Integer qqqTableId = QQQTableTableManager.getQQQTableId(getInstance().qInstance, queryStat.getTableName());
|
||||
queryStat.setQqqTableId(qqqTableId);
|
||||
|
||||
//////////////////////////////
|
||||
@ -382,7 +376,7 @@ public class QueryStatManager
|
||||
List<QueryStatJoinTable> queryStatJoinTableList = new ArrayList<>();
|
||||
for(String joinTableName : queryStat.getJoinTableNames())
|
||||
{
|
||||
queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(getQQQTableId(joinTableName)));
|
||||
queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, joinTableName)));
|
||||
}
|
||||
queryStat.setQueryStatJoinTableList(queryStatJoinTableList);
|
||||
}
|
||||
@ -460,7 +454,7 @@ public class QueryStatManager
|
||||
String[] parts = fieldName.split("\\.");
|
||||
if(parts.length > 1)
|
||||
{
|
||||
queryStatCriteriaField.setQqqTableId(getQQQTableId(parts[0]));
|
||||
queryStatCriteriaField.setQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, parts[0]));
|
||||
queryStatCriteriaField.setName(parts[1]);
|
||||
}
|
||||
}
|
||||
@ -498,7 +492,7 @@ public class QueryStatManager
|
||||
String[] parts = fieldName.split("\\.");
|
||||
if(parts.length > 1)
|
||||
{
|
||||
queryStatOrderByField.setQqqTableId(getQQQTableId(parts[0]));
|
||||
queryStatOrderByField.setQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, parts[0]));
|
||||
queryStatOrderByField.setName(parts[1]);
|
||||
}
|
||||
}
|
||||
@ -512,44 +506,6 @@ public class QueryStatManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Integer getQQQTableId(String tableName) throws QException
|
||||
{
|
||||
/////////////////////////////
|
||||
// look in the cache table //
|
||||
/////////////////////////////
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME);
|
||||
getInput.setUniqueKey(MapBuilder.of("name", tableName));
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
|
||||
////////////////////////
|
||||
// upon cache miss... //
|
||||
////////////////////////
|
||||
if(getOutput.getRecord() == null)
|
||||
{
|
||||
///////////////////////////////////////////////////////
|
||||
// insert the record (into the table, not the cache) //
|
||||
///////////////////////////////////////////////////////
|
||||
QTableMetaData tableMetaData = getInstance().qInstance.getTable(tableName);
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(QQQTable.TABLE_NAME);
|
||||
insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel())));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
|
||||
///////////////////////////////////
|
||||
// repeat the get from the cache //
|
||||
///////////////////////////////////
|
||||
getOutput = new GetAction().execute(getInput);
|
||||
}
|
||||
|
||||
return getOutput.getRecord().getValueInteger("id");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -50,6 +50,7 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
*******************************************************************************/
|
||||
public class UniqueKeyHelper
|
||||
{
|
||||
private static Integer pageSize = 1000;
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -60,62 +61,71 @@ public class UniqueKeyHelper
|
||||
Map<List<Serializable>, Serializable> existingRecords = new HashMap<>();
|
||||
if(ukFieldNames != null)
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(table.getName());
|
||||
queryInput.setTransaction(transaction);
|
||||
for(List<QRecord> page : CollectionUtils.getPages(recordList, pageSize))
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(table.getName());
|
||||
queryInput.setTransaction(transaction);
|
||||
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
if(ukFieldNames.size() == 1)
|
||||
{
|
||||
List<Serializable> values = recordList.stream()
|
||||
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors()))
|
||||
.map(r -> r.getValue(ukFieldNames.get(0)))
|
||||
.collect(Collectors.toList());
|
||||
filter.addCriteria(new QFilterCriteria(ukFieldNames.get(0), QCriteriaOperator.IN, values));
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
for(QRecord record : recordList)
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
if(ukFieldNames.size() == 1)
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
|
||||
List<Serializable> values = page.stream()
|
||||
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors()))
|
||||
.map(r -> r.getValue(ukFieldNames.get(0)))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if(values.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QQueryFilter subFilter = new QQueryFilter();
|
||||
filter.addSubFilter(subFilter);
|
||||
for(String fieldName : ukFieldNames)
|
||||
filter.addCriteria(new QFilterCriteria(ukFieldNames.get(0), QCriteriaOperator.IN, values));
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
for(QRecord record : page)
|
||||
{
|
||||
Serializable value = record.getValue(fieldName);
|
||||
if(value == null)
|
||||
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
|
||||
{
|
||||
subFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
|
||||
continue;
|
||||
}
|
||||
else
|
||||
|
||||
QQueryFilter subFilter = new QQueryFilter();
|
||||
filter.addSubFilter(subFilter);
|
||||
for(String fieldName : ukFieldNames)
|
||||
{
|
||||
subFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, value));
|
||||
Serializable value = record.getValue(fieldName);
|
||||
if(value == null)
|
||||
{
|
||||
subFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
|
||||
}
|
||||
else
|
||||
{
|
||||
subFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(CollectionUtils.nullSafeIsEmpty(filter.getSubFilters()))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we didn't build any sub-filters (because all records have errors in them), don't run a query w/ no clauses - continue to next page //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(CollectionUtils.nullSafeIsEmpty(filter.getSubFilters()))
|
||||
queryInput.setFilter(filter);
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we didn't build any sub-filters (because all records have errors in them), don't run a query w/ no clauses - rather - return early. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (existingRecords);
|
||||
}
|
||||
}
|
||||
|
||||
queryInput.setFilter(filter);
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual);
|
||||
if(keyValues.isPresent())
|
||||
{
|
||||
existingRecords.put(keyValues.get(), record.getValue(table.getPrimaryKeyField()));
|
||||
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual);
|
||||
if(keyValues.isPresent())
|
||||
{
|
||||
existingRecords.put(keyValues.get(), record.getValue(table.getPrimaryKeyField()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -200,4 +210,26 @@ public class UniqueKeyHelper
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for pageSize
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Integer getPageSize()
|
||||
{
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for pageSize
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void setPageSize(Integer pageSize)
|
||||
{
|
||||
UniqueKeyHelper.pageSize = pageSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@ -83,7 +84,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
|
||||
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action, QBackendTransaction transaction) throws QException
|
||||
{
|
||||
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, action);
|
||||
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
|
||||
@ -101,7 +102,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
// actually check lock values //
|
||||
////////////////////////////////
|
||||
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
|
||||
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys);
|
||||
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys, transaction);
|
||||
|
||||
/////////////////////////////////
|
||||
// propagate errors to records //
|
||||
@ -141,7 +142,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
** BUT - WRITE locks - in their case, we read the record no matter what, and in
|
||||
** here we need to verify we have a key that allows us to WRITE the record.
|
||||
*******************************************************************************/
|
||||
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys) throws QException
|
||||
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys, QBackendTransaction transaction) throws QException
|
||||
{
|
||||
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
|
||||
{
|
||||
@ -152,7 +153,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
|
||||
{
|
||||
treePosition.add(i);
|
||||
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys);
|
||||
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys, transaction);
|
||||
treePosition.remove(treePosition.size() - 1);
|
||||
i++;
|
||||
}
|
||||
@ -225,6 +226,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTransaction(transaction);
|
||||
queryInput.setTableName(leftMostJoin.getLeftTable());
|
||||
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
queryInput.setFilter(filter);
|
||||
|
@ -26,22 +26,32 @@ import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfOutput;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.openhtmltopdf.css.constants.IdentValue;
|
||||
import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver;
|
||||
import com.openhtmltopdf.pdfboxout.PdfBoxRenderer;
|
||||
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.helper.W3CDom;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.xhtmlrenderer.layout.SharedContext;
|
||||
import org.xhtmlrenderer.pdf.ITextRenderer;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Action to convert a string of HTML to a PDF!
|
||||
**
|
||||
** Much credit to https://www.baeldung.com/java-html-to-pdf
|
||||
*******************************************************************************/
|
||||
**
|
||||
** Updated in March 2025 to go from flying-saucer-pdf-openpdf lib to openhtmltopdf,
|
||||
** mostly to get support for max-height on images...
|
||||
********************************************************************************/
|
||||
public class ConvertHtmlToPdfAction extends AbstractQActionFunction<ConvertHtmlToPdfInput, ConvertHtmlToPdfOutput>
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ConvertHtmlToPdfAction.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -58,35 +68,37 @@ public class ConvertHtmlToPdfAction extends AbstractQActionFunction<ConvertHtmlT
|
||||
//////////////////////////////////////////////////////////////////
|
||||
Document document = Jsoup.parse(input.getHtml());
|
||||
document.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
|
||||
org.w3c.dom.Document w3cDoc = new W3CDom().fromJsoup(document);
|
||||
|
||||
//////////////////////////////
|
||||
// convert the XHTML to PDF //
|
||||
//////////////////////////////
|
||||
ITextRenderer renderer = new ITextRenderer();
|
||||
SharedContext sharedContext = renderer.getSharedContext();
|
||||
sharedContext.setPrint(true);
|
||||
sharedContext.setInteractive(false);
|
||||
PdfRendererBuilder builder = new PdfRendererBuilder();
|
||||
builder.toStream(input.getOutputStream());
|
||||
builder.useFastMode();
|
||||
builder.withW3cDocument(w3cDoc, input.getBasePath() == null ? "./" : input.getBasePath().toUri().toString());
|
||||
|
||||
if(input.getBasePath() != null)
|
||||
try(PdfBoxRenderer pdfBoxRenderer = builder.buildPdfRenderer())
|
||||
{
|
||||
String baseUrl = input.getBasePath().toUri().toURL().toString();
|
||||
renderer.setDocumentFromString(document.html(), baseUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.setDocumentFromString(document.html());
|
||||
}
|
||||
pdfBoxRenderer.layout();
|
||||
pdfBoxRenderer.getSharedContext().setPrint(true);
|
||||
pdfBoxRenderer.getSharedContext().setInteractive(false);
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// register any custom fonts the input supplied //
|
||||
//////////////////////////////////////////////////
|
||||
for(Map.Entry<String, Path> entry : CollectionUtils.nonNullMap(input.getCustomFonts()).entrySet())
|
||||
{
|
||||
renderer.getFontResolver().addFont(entry.getValue().toAbsolutePath().toString(), entry.getKey(), "UTF-8", true, null);
|
||||
}
|
||||
for(Map.Entry<String, Path> entry : CollectionUtils.nonNullMap(input.getCustomFonts()).entrySet())
|
||||
{
|
||||
LOG.warn("Note: Custom fonts appear to not be working in this class at this time...");
|
||||
pdfBoxRenderer.getFontResolver().addFont(
|
||||
entry.getValue().toAbsolutePath().toFile(), // Path to the TrueType font file
|
||||
entry.getKey(), // Font family name to use in CSS
|
||||
400, // Font weight (e.g., 400 for normal, 700 for bold)
|
||||
IdentValue.NORMAL, // Font style (e.g., NORMAL, ITALIC)
|
||||
true, // Whether to subset the font
|
||||
PdfBoxFontResolver.FontGroup.MAIN // ??
|
||||
);
|
||||
}
|
||||
|
||||
renderer.layout();
|
||||
renderer.createPDF(input.getOutputStream());
|
||||
pdfBoxRenderer.createPDF();
|
||||
}
|
||||
|
||||
return (output);
|
||||
}
|
||||
|
@ -104,10 +104,21 @@ public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplate
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Static wrapper to render a Velocity template.
|
||||
*******************************************************************************/
|
||||
@Deprecated(since = "Call the version that doesn't take an ActionInput")
|
||||
public static String renderVelocity(AbstractActionInput parentActionInput, Map<String, Object> context, String code) throws QException
|
||||
{
|
||||
return (renderVelocity(context, code));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Most convenient static wrapper to render a Velocity template.
|
||||
*******************************************************************************/
|
||||
public static String renderVelocity(AbstractActionInput parentActionInput, Map<String, Object> context, String code) throws QException
|
||||
public static String renderVelocity(Map<String, Object> context, String code) throws QException
|
||||
{
|
||||
return (render(TemplateType.VELOCITY, context, code));
|
||||
}
|
||||
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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.values;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Basic implementation of a possible value provider, for where there's a limited
|
||||
** set of possible source objects - so you just have to define how to make one
|
||||
** PV from a source object, how to list all of the source objects, and how to
|
||||
** look up a PV from an id.
|
||||
*******************************************************************************/
|
||||
public abstract class BasicCustomPossibleValueProvider<S, ID extends Serializable> implements QCustomPossibleValueProvider<ID>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected abstract QPossibleValue<ID> makePossibleValue(S sourceObject);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected abstract S getSourceObject(Serializable id);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected abstract List<S> getAllSourceObjects();
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QPossibleValue<ID> getPossibleValue(Serializable idValue)
|
||||
{
|
||||
S sourceObject = getSourceObject(idValue);
|
||||
if(sourceObject == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
return makePossibleValue(sourceObject);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QPossibleValue<ID>> search(SearchPossibleValueSourceInput input) throws QException
|
||||
{
|
||||
List<QPossibleValue<ID>> allPossibleValues = new ArrayList<>();
|
||||
List<S> allSourceObjects = getAllSourceObjects();
|
||||
for(S sourceObject : allSourceObjects)
|
||||
{
|
||||
allPossibleValues.add(makePossibleValue(sourceObject));
|
||||
}
|
||||
|
||||
return completeCustomPVSSearch(input, allPossibleValues);
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.values;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
@ -74,6 +75,31 @@ public interface QCustomPossibleValueProvider<T extends Serializable>
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** meant to be protected (but interface...) - for a custom PVS implementation
|
||||
** to complete its search (e.g., after it generates the list of PVS objects,
|
||||
** let this method do the filtering).
|
||||
***************************************************************************/
|
||||
default List<QPossibleValue<T>> completeCustomPVSSearch(SearchPossibleValueSourceInput input, List<QPossibleValue<T>> possibleValues)
|
||||
{
|
||||
SearchPossibleValueSourceAction.PreparedSearchPossibleValueSourceInput preparedInput = SearchPossibleValueSourceAction.prepareSearchPossibleValueSourceInput(input);
|
||||
|
||||
List<QPossibleValue<T>> rs = new ArrayList<>();
|
||||
|
||||
for(QPossibleValue<T> possibleValue : possibleValues)
|
||||
{
|
||||
if(possibleValue != null && SearchPossibleValueSourceAction.doesPossibleValueMatchSearchInput(possibleValue, preparedInput))
|
||||
{
|
||||
rs.add(possibleValue);
|
||||
}
|
||||
}
|
||||
|
||||
rs.sort(Comparator.nullsLast(Comparator.comparing((QPossibleValue<T> pv) -> pv.getLabel())));
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -341,7 +341,7 @@ public class QPossibleValueTranslator
|
||||
|
||||
try
|
||||
{
|
||||
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
|
||||
QCustomPossibleValueProvider<?> customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference());
|
||||
return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value)));
|
||||
}
|
||||
catch(Exception e)
|
||||
|
@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
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.commons.lang3.BooleanUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -68,7 +69,7 @@ public class QValueFormatter
|
||||
*******************************************************************************/
|
||||
public static String formatValue(QFieldMetaData field, Serializable value)
|
||||
{
|
||||
return (formatValue(field.getDisplayFormat(), field.getName(), value));
|
||||
return (formatValue(field.getDisplayFormat(), field.getType(), field.getName(), value));
|
||||
}
|
||||
|
||||
|
||||
@ -78,7 +79,7 @@ public class QValueFormatter
|
||||
*******************************************************************************/
|
||||
public static String formatValue(String displayFormat, Serializable value)
|
||||
{
|
||||
return (formatValue(displayFormat, "", value));
|
||||
return (formatValue(displayFormat, null, "", value));
|
||||
}
|
||||
|
||||
|
||||
@ -87,7 +88,7 @@ public class QValueFormatter
|
||||
** For a display format string, an optional fieldName (only used for logging),
|
||||
** and a value, apply the format.
|
||||
*******************************************************************************/
|
||||
private static String formatValue(String displayFormat, String fieldName, Serializable value)
|
||||
private static String formatValue(String displayFormat, QFieldType fieldType, String fieldName, Serializable value)
|
||||
{
|
||||
//////////////////////////////////
|
||||
// null values get null results //
|
||||
@ -107,6 +108,11 @@ public class QValueFormatter
|
||||
return formatBoolean(b);
|
||||
}
|
||||
|
||||
if(QFieldType.BOOLEAN.equals(fieldType))
|
||||
{
|
||||
return formatBoolean(ValueUtils.getValueAsBoolean(value));
|
||||
}
|
||||
|
||||
if(value instanceof LocalTime lt)
|
||||
{
|
||||
return formatLocalTime(lt);
|
||||
@ -404,6 +410,7 @@ public class QValueFormatter
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a single record, set its display values - where caller (meant to stay private)
|
||||
** can specify if they've already done fieldBehaviors (to avoid re-doing).
|
||||
@ -462,7 +469,8 @@ public class QValueFormatter
|
||||
{
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
if(field.getType().equals(QFieldType.BLOB))
|
||||
Optional<FieldAdornment> fileDownloadAdornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD);
|
||||
if(fileDownloadAdornment.isPresent())
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// file name comes from: //
|
||||
@ -472,25 +480,14 @@ public class QValueFormatter
|
||||
// - tableLabel primaryKey fieldLabel //
|
||||
// - and - if the FILE_DOWNLOAD adornment had a DEFAULT_EXTENSION, then it gets added (preceded by a dot) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Optional<FieldAdornment> fileDownloadAdornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD);
|
||||
Map<String, Serializable> adornmentValues = Collections.emptyMap();
|
||||
|
||||
if(fileDownloadAdornment.isPresent())
|
||||
{
|
||||
adornmentValues = fileDownloadAdornment.get().getValues();
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////
|
||||
// don't change blobs unless they are file-downloads //
|
||||
///////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
Map<String, Serializable> adornmentValues = fileDownloadAdornment.get().getValues();
|
||||
|
||||
String fileNameField = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FIELD));
|
||||
String fileNameFormat = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT));
|
||||
String defaultExtension = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.DEFAULT_EXTENSION));
|
||||
|
||||
Boolean downloadUrlDynamic = ValueUtils.getValueAsBoolean(adornmentValues.get(AdornmentType.FileDownloadValues.DOWNLOAD_URL_DYNAMIC));
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(!doesFieldHaveValue(field, record))
|
||||
@ -498,6 +495,11 @@ public class QValueFormatter
|
||||
continue;
|
||||
}
|
||||
|
||||
if(BooleanUtils.isTrue(downloadUrlDynamic))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
|
||||
String fileName = null;
|
||||
|
||||
@ -515,7 +517,7 @@ public class QValueFormatter
|
||||
{
|
||||
@SuppressWarnings("unchecked") // instance validation should make this safe!
|
||||
List<String> fileNameFormatFields = (List<String>) adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT_FIELDS);
|
||||
List<String> values = fileNameFormatFields.stream().map(f -> ValueUtils.getValueAsString(record.getValue(f))).toList();
|
||||
List<String> values = CollectionUtils.nullSafeHasContents(fileNameFormatFields) ? fileNameFormatFields.stream().map(f -> ValueUtils.getValueAsString(record.getValue(f))).toList() : Collections.emptyList();
|
||||
fileName = QValueFormatter.formatStringWithValues(fileNameFormat, values);
|
||||
}
|
||||
}
|
||||
@ -536,7 +538,19 @@ public class QValueFormatter
|
||||
}
|
||||
}
|
||||
|
||||
record.setValue(field.getName(), "/data/" + table.getName() + "/" + primaryKey + "/" + field.getName() + "/" + fileName);
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if field type is blob OR if there's a supplemental process or code-ref that needs to run - //
|
||||
// then update its value to be a callback-url that'll give access to the bytes to download. //
|
||||
// implied here is that a String value (w/o supplemental code/proc) has its value stay as a //
|
||||
// URL, which is where the file is directly downloaded from. And in the case of a String //
|
||||
// with code-to-run, then the code should run, followed by a redirect to the value URL. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(QFieldType.BLOB.equals(field.getType())
|
||||
|| adornmentValues.containsKey(AdornmentType.FileDownloadValues.SUPPLEMENTAL_CODE_REFERENCE)
|
||||
|| adornmentValues.containsKey(AdornmentType.FileDownloadValues.SUPPLEMENTAL_PROCESS_NAME))
|
||||
{
|
||||
record.setValue(field.getName(), AdornmentType.FileDownloadValues.makeFieldDownloadUrl(table.getName(), primaryKey, field.getName(), fileName));
|
||||
}
|
||||
record.setDisplayValue(field.getName(), fileName);
|
||||
}
|
||||
}
|
||||
@ -563,6 +577,7 @@ public class QValueFormatter
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// heavy fields that weren't fetched - they should have a backend-detail specifying their length (or null if null) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Serializable> heavyFieldLengths = (Map<String, Serializable>) record.getBackendDetail(QRecord.BACKEND_DETAILS_TYPE_HEAVY_FIELD_LENGTHS);
|
||||
if(heavyFieldLengths != null)
|
||||
{
|
||||
|
@ -26,10 +26,16 @@ import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
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.query.QCriteriaOperator;
|
||||
@ -46,10 +52,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
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.commons.lang.NotImplementedException;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -60,6 +65,9 @@ public class SearchPossibleValueSourceAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(SearchPossibleValueSourceAction.class);
|
||||
|
||||
private static final Set<String> warnedAboutUnexpectedValueField = Collections.synchronizedSet(new HashSet<>());
|
||||
private static final Set<String> warnedAboutUnexpectedNoOfFieldsToSearchByLabel = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
private QPossibleValueTranslator possibleValueTranslator;
|
||||
|
||||
|
||||
@ -69,14 +77,14 @@ public class SearchPossibleValueSourceAction
|
||||
*******************************************************************************/
|
||||
public SearchPossibleValueSourceOutput execute(SearchPossibleValueSourceInput input) throws QException
|
||||
{
|
||||
QInstance qInstance = input.getInstance();
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(input.getPossibleValueSourceName());
|
||||
if(possibleValueSource == null)
|
||||
{
|
||||
throw new QException("Missing possible value source named [" + input.getPossibleValueSourceName() + "]");
|
||||
}
|
||||
|
||||
possibleValueTranslator = new QPossibleValueTranslator(input.getInstance(), input.getSession());
|
||||
possibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
|
||||
SearchPossibleValueSourceOutput output = null;
|
||||
if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM))
|
||||
{
|
||||
@ -100,47 +108,54 @@ public class SearchPossibleValueSourceAction
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** record to store "computed" values as part of a possible-value search -
|
||||
** e.g., ids type-convered, and lower-cased labels.
|
||||
***************************************************************************/
|
||||
public record PreparedSearchPossibleValueSourceInput(Collection<?> inputIdsAsCorrectType, Collection<String> lowerCaseLabels, String searchTerm) {}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static PreparedSearchPossibleValueSourceInput prepareSearchPossibleValueSourceInput(SearchPossibleValueSourceInput input)
|
||||
{
|
||||
QPossibleValueSource possibleValueSource = QContext.getQInstance().getPossibleValueSource(input.getPossibleValueSourceName());
|
||||
List<?> inputIdsAsCorrectType = convertInputIdsToPossibleValueSourceIdType(possibleValueSource, input.getIdList());
|
||||
|
||||
Set<String> lowerCaseLabels = null;
|
||||
if(input.getLabelList() != null)
|
||||
{
|
||||
lowerCaseLabels = input.getLabelList().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(l -> l.toLowerCase())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
return (new PreparedSearchPossibleValueSourceInput(inputIdsAsCorrectType, lowerCaseLabels, input.getSearchTerm()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private SearchPossibleValueSourceOutput searchPossibleValueEnum(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource)
|
||||
{
|
||||
PreparedSearchPossibleValueSourceInput preparedSearchPossibleValueSourceInput = prepareSearchPossibleValueSourceInput(input);
|
||||
|
||||
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
|
||||
List<Serializable> matchingIds = new ArrayList<>();
|
||||
|
||||
List<?> inputIdsAsCorrectType = convertInputIdsToEnumIdType(possibleValueSource, input.getIdList());
|
||||
|
||||
for(QPossibleValue<?> possibleValue : possibleValueSource.getEnumValues())
|
||||
{
|
||||
boolean match = false;
|
||||
|
||||
if(input.getIdList() != null)
|
||||
{
|
||||
if(inputIdsAsCorrectType.contains(possibleValue.getId()))
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(StringUtils.hasContent(input.getSearchTerm()))
|
||||
{
|
||||
match = (Objects.equals(ValueUtils.getValueAsString(possibleValue.getId()).toLowerCase(), input.getSearchTerm().toLowerCase())
|
||||
|| possibleValue.getLabel().toLowerCase().startsWith(input.getSearchTerm().toLowerCase()));
|
||||
}
|
||||
else
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
}
|
||||
boolean match = doesPossibleValueMatchSearchInput(possibleValue, preparedSearchPossibleValueSourceInput);
|
||||
|
||||
if(match)
|
||||
{
|
||||
matchingIds.add((Serializable) possibleValue.getId());
|
||||
matchingIds.add(possibleValue.getId());
|
||||
}
|
||||
|
||||
// todo - skip & limit?
|
||||
// todo - default filter
|
||||
}
|
||||
|
||||
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, matchingIds);
|
||||
@ -151,37 +166,95 @@ public class SearchPossibleValueSourceAction
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static boolean doesPossibleValueMatchSearchInput(QPossibleValue<?> possibleValue, PreparedSearchPossibleValueSourceInput input)
|
||||
{
|
||||
boolean match = false;
|
||||
|
||||
if(input.inputIdsAsCorrectType() != null)
|
||||
{
|
||||
if(input.inputIdsAsCorrectType().contains(possibleValue.getId()))
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
}
|
||||
else if(input.lowerCaseLabels() != null)
|
||||
{
|
||||
if(input.lowerCaseLabels().contains(possibleValue.getLabel().toLowerCase()))
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(StringUtils.hasContent(input.searchTerm()))
|
||||
{
|
||||
match = (Objects.equals(ValueUtils.getValueAsString(possibleValue.getId()).toLowerCase(), input.searchTerm().toLowerCase())
|
||||
|| possibleValue.getLabel().toLowerCase().startsWith(input.searchTerm().toLowerCase()));
|
||||
}
|
||||
else
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The input list of ids might come through as a type that isn't the same as
|
||||
** the type of the ids in the enum (e.g., strings from a frontend, integers
|
||||
** in an enum). So, this method looks at the first id in the enum, and then
|
||||
** maps all the inputIds to be of the same type.
|
||||
** in an enum). So, this method type-converts them.
|
||||
*******************************************************************************/
|
||||
private List<Object> convertInputIdsToEnumIdType(QPossibleValueSource possibleValueSource, List<Serializable> inputIdList)
|
||||
private static List<Object> convertInputIdsToPossibleValueSourceIdType(QPossibleValueSource possibleValueSource, List<Serializable> inputIdList)
|
||||
{
|
||||
List<Object> rs = new ArrayList<>();
|
||||
if(CollectionUtils.nullSafeIsEmpty(inputIdList))
|
||||
|
||||
if(inputIdList == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
else if(inputIdList.isEmpty())
|
||||
{
|
||||
return (rs);
|
||||
}
|
||||
|
||||
Object anIdFromTheEnum = possibleValueSource.getEnumValues().get(0).getId();
|
||||
QFieldType type = possibleValueSource.getIdType();
|
||||
|
||||
if(anIdFromTheEnum instanceof Integer)
|
||||
for(Serializable inputId : inputIdList)
|
||||
{
|
||||
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsInteger(id)));
|
||||
}
|
||||
else if(anIdFromTheEnum instanceof String)
|
||||
{
|
||||
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsString(id)));
|
||||
}
|
||||
else if(anIdFromTheEnum instanceof Boolean)
|
||||
{
|
||||
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsBoolean(id)));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Unexpected type [" + anIdFromTheEnum.getClass().getSimpleName() + "] for ids in enum: " + possibleValueSource.getName());
|
||||
Object properlyTypedId = null;
|
||||
try
|
||||
{
|
||||
if(type.equals(QFieldType.INTEGER))
|
||||
{
|
||||
properlyTypedId = ValueUtils.getValueAsInteger(inputId);
|
||||
}
|
||||
else if(type.isStringLike())
|
||||
{
|
||||
properlyTypedId = ValueUtils.getValueAsString(inputId);
|
||||
}
|
||||
else if(type.equals(QFieldType.BOOLEAN))
|
||||
{
|
||||
properlyTypedId = ValueUtils.getValueAsBoolean(inputId);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Unexpected type [" + type + "] for ids in enum: " + possibleValueSource.getName());
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Error converting possible value id to expected id type", e, logPair("value", inputId));
|
||||
}
|
||||
|
||||
if(properlyTypedId != null)
|
||||
{
|
||||
rs.add(properlyTypedId);
|
||||
}
|
||||
}
|
||||
|
||||
return (rs);
|
||||
@ -199,7 +272,7 @@ public class SearchPossibleValueSourceAction
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(possibleValueSource.getTableName());
|
||||
|
||||
QTableMetaData table = input.getInstance().getTable(possibleValueSource.getTableName());
|
||||
QTableMetaData table = QContext.getQInstance().getTable(possibleValueSource.getTableName());
|
||||
|
||||
QQueryFilter queryFilter = new QQueryFilter();
|
||||
queryFilter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
@ -208,6 +281,53 @@ public class SearchPossibleValueSourceAction
|
||||
{
|
||||
queryFilter.addCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, input.getIdList()));
|
||||
}
|
||||
else if(input.getLabelList() != null)
|
||||
{
|
||||
List<String> fieldNames = new ArrayList<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the 'value fields' will either be 'id' or 'label' (which means, use the fields from the tableMetaData's label fields) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(String valueField : possibleValueSource.getValueFields())
|
||||
{
|
||||
if("id".equals(valueField))
|
||||
{
|
||||
fieldNames.add(table.getPrimaryKeyField());
|
||||
}
|
||||
else if("label".equals(valueField))
|
||||
{
|
||||
if(table.getRecordLabelFields() != null)
|
||||
{
|
||||
fieldNames.addAll(table.getRecordLabelFields());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String message = "Unexpected valueField defined in possibleValueSource when searching possibleValueSource by label (required: 'id' or 'label')";
|
||||
if(!warnedAboutUnexpectedValueField.contains(possibleValueSource.getName()))
|
||||
{
|
||||
LOG.warn(message, logPair("valueField", valueField), logPair("possibleValueSource", possibleValueSource.getName()));
|
||||
warnedAboutUnexpectedValueField.add(possibleValueSource.getName());
|
||||
}
|
||||
output.setWarning(message);
|
||||
}
|
||||
}
|
||||
|
||||
if(fieldNames.size() == 1)
|
||||
{
|
||||
queryFilter.addCriteria(new QFilterCriteria(fieldNames.get(0), QCriteriaOperator.IN, input.getLabelList()));
|
||||
}
|
||||
else
|
||||
{
|
||||
String message = "Unexpected number of fields found for searching possibleValueSource by label (required: 1, found: " + fieldNames.size() + ")";
|
||||
if(!warnedAboutUnexpectedNoOfFieldsToSearchByLabel.contains(possibleValueSource.getName()))
|
||||
{
|
||||
LOG.warn(message);
|
||||
warnedAboutUnexpectedNoOfFieldsToSearchByLabel.add(possibleValueSource.getName());
|
||||
}
|
||||
output.setWarning(message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String searchTerm = input.getSearchTerm();
|
||||
@ -259,9 +379,6 @@ public class SearchPossibleValueSourceAction
|
||||
}
|
||||
}
|
||||
|
||||
// todo - skip & limit as params
|
||||
queryFilter.setLimit(250);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if given a default filter, make it the 'top level' filter and the one we just created a subfilter //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -271,6 +388,9 @@ public class SearchPossibleValueSourceAction
|
||||
queryFilter = input.getDefaultQueryFilter();
|
||||
}
|
||||
|
||||
queryFilter.setLimit(input.getLimit());
|
||||
queryFilter.setSkip(input.getSkip());
|
||||
|
||||
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
|
||||
|
||||
queryInput.setFilter(queryFilter);
|
||||
@ -287,7 +407,7 @@ public class SearchPossibleValueSourceAction
|
||||
fieldName = table.getPrimaryKeyField();
|
||||
}
|
||||
|
||||
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(fieldName)).toList();
|
||||
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(fieldName)).toList();
|
||||
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, ids);
|
||||
output.setResults(qPossibleValues);
|
||||
|
||||
@ -299,11 +419,12 @@ public class SearchPossibleValueSourceAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private SearchPossibleValueSourceOutput searchPossibleValueCustom(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource)
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private SearchPossibleValueSourceOutput searchPossibleValueCustom(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
|
||||
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference());
|
||||
List<QPossibleValue<?>> possibleValues = customPossibleValueProvider.search(input);
|
||||
|
||||
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
|
||||
@ -312,11 +433,10 @@ public class SearchPossibleValueSourceAction
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
// LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e);
|
||||
String message = "Error searching custom possible value source [" + input.getPossibleValueSourceName() + "]";
|
||||
LOG.warn(message, e);
|
||||
throw (new QException(message, e));
|
||||
}
|
||||
|
||||
throw new NotImplementedException("Not impleemnted");
|
||||
// return (null);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,8 +22,8 @@
|
||||
package com.kingsrook.qqq.backend.core.exceptions;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
@ -55,12 +55,11 @@ public class QInstanceValidationException extends QException
|
||||
*******************************************************************************/
|
||||
public QInstanceValidationException(List<String> reasons)
|
||||
{
|
||||
super(
|
||||
(reasons != null && reasons.size() > 0)
|
||||
? "Instance validation failed for the following reasons:\n - " + StringUtils.join("\n - ", reasons)
|
||||
: "Validation failed, but no reasons were provided");
|
||||
super((CollectionUtils.nullSafeHasContents(reasons))
|
||||
? "Instance validation failed for the following reasons:\n - " + StringUtils.join("\n - ", reasons) + "\n(" + reasons.size() + " Total reason" + StringUtils.plural(reasons) + ")"
|
||||
: "Validation failed, but no reasons were provided");
|
||||
|
||||
if(reasons != null && reasons.size() > 0)
|
||||
if(CollectionUtils.nullSafeHasContents(reasons))
|
||||
{
|
||||
this.reasons = reasons;
|
||||
}
|
||||
@ -68,25 +67,6 @@ public class QInstanceValidationException extends QException
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor of an array/varargs of reasons. They feed into the core exception message.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QInstanceValidationException(String... reasons)
|
||||
{
|
||||
super(
|
||||
(reasons != null && reasons.length > 0)
|
||||
? "Instance validation failed for the following reasons: " + StringUtils.joinWithCommasAndAnd(Arrays.stream(reasons).toList())
|
||||
: "Validation failed, but no reasons were provided");
|
||||
|
||||
if(reasons != null && reasons.length > 0)
|
||||
{
|
||||
this.reasons = Arrays.stream(reasons).toList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor of message & cause - does not populate reasons!
|
||||
**
|
||||
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Version of AbstractQQQApplication that assumes all meta-data is produced
|
||||
** by MetaDataProducers in (or under) a single package.
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractMetaDataProducerBasedQQQApplication extends AbstractQQQApplication
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public abstract String getMetaDataPackageName();
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QInstance defineQInstance() throws QException
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance, getMetaDataPackageName());
|
||||
return (qInstance);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
import com.kingsrook.qqq.backend.core.instances.validation.plugins.QInstanceValidatorPluginInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Base class to provide the definition of a QQQ-based application.
|
||||
**
|
||||
** Essentially, just how to define its meta-data - in the form of a QInstance.
|
||||
**
|
||||
** Also provides means to define the instance validation plugins to be used.
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractQQQApplication
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public abstract QInstance defineQInstance() throws QException;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public QInstance defineValidatedQInstance() throws QException, QInstanceValidationException
|
||||
{
|
||||
QInstance qInstance = defineQInstance();
|
||||
|
||||
QInstanceValidator.removeAllValidatorPlugins();
|
||||
for(QInstanceValidatorPluginInterface<?> validatorPlugin : CollectionUtils.nonNullList(getValidatorPlugins()))
|
||||
{
|
||||
QInstanceValidator.addValidatorPlugin(validatorPlugin);
|
||||
}
|
||||
|
||||
QInstanceValidator qInstanceValidator = new QInstanceValidator();
|
||||
qInstanceValidator.validate(qInstance);
|
||||
return (qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected List<QInstanceValidatorPluginInterface<?>> getValidatorPlugins()
|
||||
{
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.MetaDataLoaderHelper;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Version of AbstractQQQApplication that assumes all meta-data is defined in
|
||||
** config files (yaml, json, etc) under a given directory path.
|
||||
*******************************************************************************/
|
||||
public class ConfigFilesBasedQQQApplication extends AbstractQQQApplication
|
||||
{
|
||||
private final String path;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ConfigFilesBasedQQQApplication(String path)
|
||||
{
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QInstance defineQInstance() throws QException
|
||||
{
|
||||
QInstance qInstance = new QInstance();
|
||||
MetaDataLoaderHelper.processAllMetaDataFilesInDirectory(qInstance, path);
|
||||
return (qInstance);
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Version of AbstractQQQApplication that assumes all meta-data is produced
|
||||
** by MetaDataProducers in (or under) a single package (where you can pass that
|
||||
** package into the constructor, vs. the abstract base class, where you extend
|
||||
** it and override the getMetaDataPackageName method.
|
||||
*******************************************************************************/
|
||||
public class MetaDataProducerBasedQQQApplication extends AbstractMetaDataProducerBasedQQQApplication
|
||||
{
|
||||
private final String metaDataPackageName;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public MetaDataProducerBasedQQQApplication(String metaDataPackageName)
|
||||
{
|
||||
this.metaDataPackageName = metaDataPackageName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public MetaDataProducerBasedQQQApplication(Class<?> aClassInMetaDataPackage)
|
||||
{
|
||||
this(aClassInMetaDataPackage.getPackageName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public String getMetaDataPackageName()
|
||||
{
|
||||
return (this.metaDataPackageName);
|
||||
}
|
||||
}
|
@ -23,7 +23,11 @@ package com.kingsrook.qqq.backend.core.instances;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@ -31,21 +35,27 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.BulkTableActionProcessPermissionChecker;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.enrichment.plugins.QInstanceEnricherPluginInterface;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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.code.QCodeReference;
|
||||
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.AdornmentType.FileUploadAdornment;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
@ -54,10 +64,12 @@ import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPer
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep;
|
||||
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.reporting.QReportDataSource;
|
||||
@ -74,13 +86,20 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEd
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertExtractStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertLoadStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertPrepareFileMappingStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertPrepareFileUploadStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertPrepareValueMappingStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertReceiveFileMappingStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertReceiveValueMappingStep;
|
||||
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.ClassPathUtils;
|
||||
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 static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -106,6 +125,8 @@ public class QInstanceEnricher
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private static final Map<String, String> labelMappings = new LinkedHashMap<>();
|
||||
|
||||
private static ListingHash<Class<?>, QInstanceEnricherPluginInterface<?>> enricherPlugins = new ListingHash<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -167,6 +188,7 @@ public class QInstanceEnricher
|
||||
}
|
||||
|
||||
enrichJoins();
|
||||
enrichInstance();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// if the instance DOES have 1 or more scheduler, but no schedulable types, //
|
||||
@ -183,6 +205,16 @@ public class QInstanceEnricher
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void enrichInstance()
|
||||
{
|
||||
runPlugins(QInstance.class, qInstance, qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -247,6 +279,14 @@ public class QInstanceEnricher
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////
|
||||
// run plugins on joins if there are any //
|
||||
///////////////////////////////////////////
|
||||
for(QJoinMetaData join : qInstance.getJoins().values())
|
||||
{
|
||||
runPlugins(QJoinMetaData.class, join, qInstance);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -262,6 +302,7 @@ public class QInstanceEnricher
|
||||
private void enrichWidget(QWidgetMetaDataInterface widgetMetaData)
|
||||
{
|
||||
enrichPermissionRules(widgetMetaData);
|
||||
runPlugins(QWidgetMetaDataInterface.class, widgetMetaData, qInstance);
|
||||
}
|
||||
|
||||
|
||||
@ -272,6 +313,7 @@ public class QInstanceEnricher
|
||||
private void enrichBackend(QBackendMetaData qBackendMetaData)
|
||||
{
|
||||
qBackendMetaData.enrich();
|
||||
runPlugins(QBackendMetaData.class, qBackendMetaData, qInstance);
|
||||
}
|
||||
|
||||
|
||||
@ -288,7 +330,21 @@ public class QInstanceEnricher
|
||||
|
||||
if(table.getFields() != null)
|
||||
{
|
||||
table.getFields().values().forEach(this::enrichField);
|
||||
for(Map.Entry<String, QFieldMetaData> entry : table.getFields().entrySet())
|
||||
{
|
||||
String name = entry.getKey();
|
||||
QFieldMetaData field = entry.getValue();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// in case the field wasn't given a name, use its key from the fields map //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
if(!StringUtils.hasContent(field.getName()))
|
||||
{
|
||||
field.setName(name);
|
||||
}
|
||||
|
||||
enrichField(field);
|
||||
}
|
||||
|
||||
for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values())
|
||||
{
|
||||
@ -312,6 +368,7 @@ public class QInstanceEnricher
|
||||
|
||||
enrichPermissionRules(table);
|
||||
enrichAuditRules(table);
|
||||
runPlugins(QTableMetaData.class, table, qInstance);
|
||||
}
|
||||
|
||||
|
||||
@ -402,6 +459,7 @@ public class QInstanceEnricher
|
||||
}
|
||||
|
||||
enrichPermissionRules(process);
|
||||
runPlugins(QProcessMetaData.class, process, qInstance);
|
||||
}
|
||||
|
||||
|
||||
@ -410,10 +468,27 @@ public class QInstanceEnricher
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void enrichStep(QStepMetaData step)
|
||||
{
|
||||
enrichStep(step, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void enrichStep(QStepMetaData step, boolean isSubStep)
|
||||
{
|
||||
if(!StringUtils.hasContent(step.getLabel()))
|
||||
{
|
||||
step.setLabel(nameToLabel(step.getName()));
|
||||
if(isSubStep && (step.getName().endsWith(".backend") || step.getName().endsWith(".frontend")))
|
||||
{
|
||||
step.setLabel(nameToLabel(step.getName().replaceFirst("\\.(backend|frontend)", "")));
|
||||
}
|
||||
else
|
||||
{
|
||||
step.setLabel(nameToLabel(step.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
step.getInputFields().forEach(this::enrichField);
|
||||
@ -434,6 +509,13 @@ public class QInstanceEnricher
|
||||
frontendStepMetaData.getRecordListFields().forEach(this::enrichField);
|
||||
}
|
||||
}
|
||||
else if(step instanceof QStateMachineStep stateMachineStep)
|
||||
{
|
||||
for(QStepMetaData subStep : CollectionUtils.nonNullList(stateMachineStep.getSubSteps()))
|
||||
{
|
||||
enrichStep(subStep, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -499,6 +581,8 @@ public class QInstanceEnricher
|
||||
field.withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE);
|
||||
}
|
||||
}
|
||||
|
||||
runPlugins(QFieldMetaData.class, field, qInstance);
|
||||
}
|
||||
|
||||
|
||||
@ -570,6 +654,7 @@ public class QInstanceEnricher
|
||||
ensureAppSectionMembersAreAppChildren(app);
|
||||
|
||||
enrichPermissionRules(app);
|
||||
runPlugins(QAppMetaData.class, app, qInstance);
|
||||
}
|
||||
|
||||
|
||||
@ -717,6 +802,7 @@ public class QInstanceEnricher
|
||||
}
|
||||
|
||||
enrichPermissionRules(report);
|
||||
runPlugins(QReportMetaData.class, report, qInstance);
|
||||
}
|
||||
|
||||
|
||||
@ -808,7 +894,7 @@ public class QInstanceEnricher
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void defineTableBulkInsert(QInstance qInstance, QTableMetaData table, String processName)
|
||||
public void defineTableBulkInsert(QInstance qInstance, QTableMetaData table, String processName)
|
||||
{
|
||||
Map<String, Serializable> values = new HashMap<>();
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, table.getName());
|
||||
@ -820,6 +906,7 @@ public class QInstanceEnricher
|
||||
values
|
||||
)
|
||||
.withName(processName)
|
||||
.withIcon(new QIcon().withName("library_add"))
|
||||
.withLabel(table.getLabel() + " Bulk Insert")
|
||||
.withTableName(table.getName())
|
||||
.withIsHidden(true)
|
||||
@ -850,18 +937,76 @@ public class QInstanceEnricher
|
||||
.map(QFieldMetaData::getLabel)
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
QBackendStepMetaData prepareFileUploadStep = new QBackendStepMetaData()
|
||||
.withName("prepareFileUpload")
|
||||
.withCode(new QCodeReference(BulkInsertPrepareFileUploadStep.class));
|
||||
|
||||
QFrontendStepMetaData uploadScreen = new QFrontendStepMetaData()
|
||||
.withName("upload")
|
||||
.withLabel("Upload File")
|
||||
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB).withLabel(table.getLabel() + " File").withIsRequired(true))
|
||||
.withComponent(new QFrontendComponentMetaData()
|
||||
.withType(QComponentType.HELP_TEXT)
|
||||
.withValue("previewText", "file upload instructions")
|
||||
.withValue("text", "Upload a CSV file with the following columns:\n" + fieldsForHelpText))
|
||||
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB)
|
||||
.withFieldAdornment(FileUploadAdornment.newFieldAdornment()
|
||||
.withValue(FileUploadAdornment.formatDragAndDrop())
|
||||
.withValue(FileUploadAdornment.widthFull()))
|
||||
.withLabel(table.getLabel() + " File")
|
||||
.withIsRequired(true))
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.HTML))
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
|
||||
|
||||
process.addStep(0, uploadScreen);
|
||||
process.getFrontendStep("review").setRecordListFields(editableFields);
|
||||
QBackendStepMetaData prepareFileMappingStep = new QBackendStepMetaData()
|
||||
.withName("prepareFileMapping")
|
||||
.withCode(new QCodeReference(BulkInsertPrepareFileMappingStep.class));
|
||||
|
||||
QFrontendStepMetaData fileMappingScreen = new QFrontendStepMetaData()
|
||||
.withName("fileMapping")
|
||||
.withLabel("File Mapping")
|
||||
.withBackStepName("prepareFileUpload")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_FILE_MAPPING_FORM))
|
||||
.withFormField(new QFieldMetaData("hasHeaderRow", QFieldType.BOOLEAN))
|
||||
.withFormField(new QFieldMetaData("layout", QFieldType.STRING)); // is actually PVS, but, this field is only added to help support helpContent, so :shrug:
|
||||
|
||||
QBackendStepMetaData receiveFileMappingStep = new QBackendStepMetaData()
|
||||
.withName("receiveFileMapping")
|
||||
.withCode(new QCodeReference(BulkInsertReceiveFileMappingStep.class));
|
||||
|
||||
QBackendStepMetaData prepareValueMappingStep = new QBackendStepMetaData()
|
||||
.withName("prepareValueMapping")
|
||||
.withCode(new QCodeReference(BulkInsertPrepareValueMappingStep.class));
|
||||
|
||||
QFrontendStepMetaData valueMappingScreen = new QFrontendStepMetaData()
|
||||
.withName("valueMapping")
|
||||
.withLabel("Value Mapping")
|
||||
.withBackStepName("prepareFileMapping")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_VALUE_MAPPING_FORM));
|
||||
|
||||
QBackendStepMetaData receiveValueMappingStep = new QBackendStepMetaData()
|
||||
.withName("receiveValueMapping")
|
||||
.withCode(new QCodeReference(BulkInsertReceiveValueMappingStep.class));
|
||||
|
||||
int i = 0;
|
||||
process.addStep(i++, prepareFileUploadStep);
|
||||
process.addStep(i++, uploadScreen);
|
||||
|
||||
process.addStep(i++, prepareFileMappingStep);
|
||||
process.addStep(i++, fileMappingScreen);
|
||||
process.addStep(i++, receiveFileMappingStep);
|
||||
|
||||
process.addStep(i++, prepareValueMappingStep);
|
||||
process.addStep(i++, valueMappingScreen);
|
||||
process.addStep(i++, receiveValueMappingStep);
|
||||
|
||||
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW).setRecordListFields(editableFields);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// put the bulk-load profile form (e.g., for saving it) on the review & result screens) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW)
|
||||
.withBackStepName("prepareFileMapping")
|
||||
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
|
||||
|
||||
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_RESULT)
|
||||
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
|
||||
|
||||
qInstance.addProcess(process);
|
||||
}
|
||||
|
||||
@ -1256,6 +1401,137 @@ public class QInstanceEnricher
|
||||
}
|
||||
}
|
||||
|
||||
if(possibleValueSource.getIdType() == null)
|
||||
{
|
||||
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
|
||||
if(table != null)
|
||||
{
|
||||
String primaryKeyField = table.getPrimaryKeyField();
|
||||
QFieldMetaData primaryKeyFieldMetaData = table.getFields().get(primaryKeyField);
|
||||
if(primaryKeyFieldMetaData != null)
|
||||
{
|
||||
possibleValueSource.setIdType(primaryKeyFieldMetaData.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(QPossibleValueSourceType.ENUM.equals(possibleValueSource.getType()))
|
||||
{
|
||||
if(possibleValueSource.getIdType() == null)
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()))
|
||||
{
|
||||
Object id = possibleValueSource.getEnumValues().get(0).getId();
|
||||
try
|
||||
{
|
||||
possibleValueSource.setIdType(QFieldType.fromClass(id.getClass()));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error enriching possible value source with idType based on first enum value", e, logPair("possibleValueSource", possibleValueSource.getName()), logPair("id", id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(QPossibleValueSourceType.CUSTOM.equals(possibleValueSource.getType()))
|
||||
{
|
||||
if(possibleValueSource.getIdType() == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
QCustomPossibleValueProvider<?> customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference());
|
||||
|
||||
Method getPossibleValueMethod = customPossibleValueProvider.getClass().getDeclaredMethod("getPossibleValue", Serializable.class);
|
||||
Type returnType = getPossibleValueMethod.getGenericReturnType();
|
||||
Type idType = ((ParameterizedType) returnType).getActualTypeArguments()[0];
|
||||
|
||||
if(idType instanceof Class<?> c)
|
||||
{
|
||||
possibleValueSource.setIdType(QFieldType.fromClass(c));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error enriching possible value source with idType based on first custom value", e, logPair("possibleValueSource", possibleValueSource.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void addEnricherPlugin(QInstanceEnricherPluginInterface<?> plugin)
|
||||
{
|
||||
Optional<Method> enrichMethod = Arrays.stream(plugin.getClass().getDeclaredMethods())
|
||||
.filter(m -> m.getName().equals("enrich")
|
||||
&& m.getParameterCount() == 2
|
||||
&& !m.getParameterTypes()[0].equals(Object.class)
|
||||
&& m.getParameterTypes()[1].equals(QInstance.class)
|
||||
).findFirst();
|
||||
|
||||
if(enrichMethod.isPresent())
|
||||
{
|
||||
Class<?> parameterType = enrichMethod.get().getParameterTypes()[0];
|
||||
enricherPlugins.add(parameterType, plugin);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Could not find enrich method on enricher plugin [" + plugin.getClass().getName() + "] (to infer type being enriched) - this plugin will not be used.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void removeAllEnricherPlugins()
|
||||
{
|
||||
enricherPlugins.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** scan the classpath for classes in the specified package name which
|
||||
** implement the QInstanceEnricherPluginInterface - any found get added
|
||||
***************************************************************************/
|
||||
public static void discoverAndAddPluginsInPackage(String packageName) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
for(Class<?> aClass : ClassPathUtils.getClassesInPackage(packageName))
|
||||
{
|
||||
if(QInstanceEnricherPluginInterface.class.isAssignableFrom(aClass))
|
||||
{
|
||||
QInstanceEnricherPluginInterface<?> plugin = (QInstanceEnricherPluginInterface<?>) aClass.getConstructor().newInstance();
|
||||
addEnricherPlugin(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QException("Error discovering and adding enricher plugins in package [" + packageName + "]", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private <T> void runPlugins(Class<T> c, T t, QInstance qInstance)
|
||||
{
|
||||
for(QInstanceEnricherPluginInterface<?> plugin : CollectionUtils.nonNullList(enricherPlugins.get(c)))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
QInstanceEnricherPluginInterface<T> castedPlugin = (QInstanceEnricherPluginInterface<T>) plugin;
|
||||
castedPlugin.enrich(t, qInstance);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,6 @@ 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;
|
||||
@ -111,7 +110,7 @@ public class QInstanceHelpContentManager
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Discarding help content with key that does not contain name:value format", logPair("key", key), logPair("id", record.getValue("id")));
|
||||
LOG.info("Discarding help content with key-part that does not contain name:value format", logPair("key", key), logPair("part", part), logPair("id", record.getValue("id")));
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,19 +149,19 @@ public class QInstanceHelpContentManager
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
if(StringUtils.hasContent(tableName))
|
||||
{
|
||||
processHelpContentForTable(key, tableName, sectionName, fieldName, slotName, roles, helpContent);
|
||||
processHelpContentForTable(qInstance, key, tableName, sectionName, fieldName, slotName, roles, helpContent);
|
||||
}
|
||||
else if(StringUtils.hasContent(processName))
|
||||
{
|
||||
processHelpContentForProcess(key, processName, fieldName, stepName, roles, helpContent);
|
||||
processHelpContentForProcess(qInstance, key, processName, fieldName, stepName, roles, helpContent);
|
||||
}
|
||||
else if(StringUtils.hasContent(widgetName))
|
||||
{
|
||||
processHelpContentForWidget(key, widgetName, slotName, roles, helpContent);
|
||||
processHelpContentForWidget(qInstance, key, widgetName, slotName, roles, helpContent);
|
||||
}
|
||||
else if(nameValuePairs.containsKey("instanceLevel"))
|
||||
{
|
||||
processHelpContentForInstance(key, slotName, roles, helpContent);
|
||||
processHelpContentForInstance(qInstance, key, slotName, roles, helpContent);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -176,9 +175,9 @@ public class QInstanceHelpContentManager
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForTable(String key, String tableName, String sectionName, String fieldName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
private static void processHelpContentForTable(QInstance qInstance, String key, String tableName, String sectionName, String fieldName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
QTableMetaData table = qInstance.getTable(tableName);
|
||||
if(table == null)
|
||||
{
|
||||
LOG.info("Unrecognized table in help content", logPair("key", key));
|
||||
@ -246,9 +245,30 @@ public class QInstanceHelpContentManager
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForProcess(String key, String processName, String fieldName, String stepName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
private static void processHelpContentForProcess(QInstance qInstance, String key, String processName, String fieldName, String stepName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
if(processName.startsWith("*") && processName.length() > 1)
|
||||
{
|
||||
boolean anyMatched = false;
|
||||
String subName = processName.substring(1);
|
||||
for(QProcessMetaData process : qInstance.getProcesses().values())
|
||||
{
|
||||
if(process.getName().endsWith(subName))
|
||||
{
|
||||
anyMatched = true;
|
||||
processHelpContentForProcess(qInstance, key, process.getName(), fieldName, stepName, roles, helpContent);
|
||||
}
|
||||
}
|
||||
|
||||
if(!anyMatched)
|
||||
{
|
||||
LOG.info("Wildcard process name did not match any processes in help content", logPair("key", key));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
QProcessMetaData process = qInstance.getProcess(processName);
|
||||
if(process == null)
|
||||
{
|
||||
LOG.info("Unrecognized process in help content", logPair("key", key));
|
||||
@ -306,9 +326,9 @@ public class QInstanceHelpContentManager
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForWidget(String key, String widgetName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
private static void processHelpContentForWidget(QInstance qInstance, String key, String widgetName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
|
||||
QWidgetMetaDataInterface widget = qInstance.getWidget(widgetName);
|
||||
if(!StringUtils.hasContent(slotName))
|
||||
{
|
||||
LOG.info("Missing slot name in help content", logPair("key", key));
|
||||
@ -335,7 +355,7 @@ public class QInstanceHelpContentManager
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForInstance(String key, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
private static void processHelpContentForInstance(QInstance qInstance, String key, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
if(!StringUtils.hasContent(slotName))
|
||||
{
|
||||
@ -345,11 +365,11 @@ public class QInstanceHelpContentManager
|
||||
{
|
||||
if(helpContent != null)
|
||||
{
|
||||
QContext.getQInstance().withHelpContent(slotName, helpContent);
|
||||
qInstance.withHelpContent(slotName, helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
QContext.getQInstance().removeHelpContent(slotName, roles);
|
||||
qInstance.removeHelpContent(slotName, roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,13 +37,18 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
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.metadata.MetaDataActionCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataFilterInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface;
|
||||
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;
|
||||
@ -59,6 +64,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaDa
|
||||
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.QCodeReferenceLambda;
|
||||
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;
|
||||
@ -68,11 +74,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
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.possiblevalues.QPossibleValueSourceType;
|
||||
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;
|
||||
@ -104,12 +112,16 @@ 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.model.metadata.variants.BackendVariantSetting;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsConfig;
|
||||
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.UnsafeFunction;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import org.quartz.CronExpression;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
@ -132,6 +144,8 @@ public class QInstanceValidator
|
||||
|
||||
private static ListingHash<Class<?>, QInstanceValidatorPluginInterface<?>> validatorPlugins = new ListingHash<>();
|
||||
|
||||
private JoinGraph joinGraph = null;
|
||||
|
||||
private List<String> errors = new ArrayList<>();
|
||||
|
||||
|
||||
@ -159,8 +173,7 @@ public class QInstanceValidator
|
||||
// the enricher will build a join graph (if there are any joins). we'd like to only do that //
|
||||
// once, during the enrichment/validation work, so, capture it, and store it back in the instance. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
JoinGraph joinGraph = null;
|
||||
long start = System.currentTimeMillis();
|
||||
long start = System.currentTimeMillis();
|
||||
try
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -169,7 +182,7 @@ public class QInstanceValidator
|
||||
// TODO - possible point of customization (use a different enricher, or none, or pass it options).
|
||||
QInstanceEnricher qInstanceEnricher = new QInstanceEnricher(qInstance);
|
||||
qInstanceEnricher.enrich();
|
||||
joinGraph = qInstanceEnricher.getJoinGraph();
|
||||
this.joinGraph = qInstanceEnricher.getJoinGraph();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -184,6 +197,7 @@ public class QInstanceValidator
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
validateInstanceAttributes(qInstance);
|
||||
validateBackends(qInstance);
|
||||
validateAuthentication(qInstance);
|
||||
validateAutomationProviders(qInstance);
|
||||
@ -224,6 +238,24 @@ public class QInstanceValidator
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void validateInstanceAttributes(QInstance qInstance)
|
||||
{
|
||||
if(qInstance.getMetaDataFilter() != null)
|
||||
{
|
||||
validateSimpleCodeReference("Instance metaDataFilter ", qInstance.getMetaDataFilter(), MetaDataFilterInterface.class);
|
||||
}
|
||||
|
||||
if(qInstance.getMetaDataActionCustomizer() != null)
|
||||
{
|
||||
validateSimpleCodeReference("Instance metaDataActionCustomizer ", qInstance.getMetaDataActionCustomizer(), MetaDataActionCustomizerInterface.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -355,8 +387,8 @@ public class QInstanceValidator
|
||||
assertCondition(join.getType() != null, "Missing type for join: " + joinName);
|
||||
assertCondition(CollectionUtils.nullSafeHasContents(join.getJoinOns()), "Missing joinOns for join: " + joinName);
|
||||
|
||||
boolean leftTableExists = assertCondition(qInstance.getTable(join.getLeftTable()) != null, "Left-table name " + join.getLeftTable() + " join " + joinName + " is not a defined table in this instance.");
|
||||
boolean rightTableExists = assertCondition(qInstance.getTable(join.getRightTable()) != null, "Right-table name " + join.getRightTable() + " join " + joinName + " is not a defined table in this instance.");
|
||||
boolean leftTableExists = assertCondition(qInstance.getTable(join.getLeftTable()) != null, "Left-table name " + join.getLeftTable() + " in join " + joinName + " is not a defined table in this instance.");
|
||||
boolean rightTableExists = assertCondition(qInstance.getTable(join.getRightTable()) != null, "Right-table name " + join.getRightTable() + " in join " + joinName + " is not a defined table in this instance.");
|
||||
|
||||
for(JoinOn joinOn : CollectionUtils.nonNullList(join.getJoinOns()))
|
||||
{
|
||||
@ -525,6 +557,60 @@ public class QInstanceValidator
|
||||
{
|
||||
assertCondition(Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + ".");
|
||||
|
||||
///////////////////////
|
||||
// validate variants //
|
||||
///////////////////////
|
||||
BackendVariantsConfig backendVariantsConfig = backend.getBackendVariantsConfig();
|
||||
if(BooleanUtils.isTrue(backend.getUsesVariants()))
|
||||
{
|
||||
if(assertCondition(backendVariantsConfig != null, "Missing backendVariantsConfig in backend [" + backendName + "] which is marked as usesVariants"))
|
||||
{
|
||||
assertCondition(StringUtils.hasContent(backendVariantsConfig.getVariantTypeKey()), "Missing variantTypeKey in backendVariantsConfig in [" + backendName + "]");
|
||||
|
||||
String optionsTableName = backendVariantsConfig.getOptionsTableName();
|
||||
QTableMetaData optionsTable = qInstance.getTable(optionsTableName);
|
||||
if(assertCondition(StringUtils.hasContent(optionsTableName), "Missing optionsTableName in backendVariantsConfig in [" + backendName + "]"))
|
||||
{
|
||||
if(assertCondition(optionsTable != null, "Unrecognized optionsTableName [" + optionsTableName + "] in backendVariantsConfig in [" + backendName + "]"))
|
||||
{
|
||||
QQueryFilter optionsFilter = backendVariantsConfig.getOptionsFilter();
|
||||
if(optionsFilter != null)
|
||||
{
|
||||
validateQueryFilter(qInstance, "optionsFilter in backendVariantsConfig in backend [" + backendName + "]: ", optionsTable, optionsFilter, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<BackendVariantSetting, String> backendSettingSourceFieldNameMap = backendVariantsConfig.getBackendSettingSourceFieldNameMap();
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(backendSettingSourceFieldNameMap), "Missing or empty backendSettingSourceFieldNameMap in backendVariantsConfig in [" + backendName + "]"))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// only validate field names in the backendSettingSourceFieldNameMap if there is NOT a variantRecordSupplier //
|
||||
// (the idea being, that the supplier might be building a record with fieldNames that aren't in the table... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(optionsTable != null && backendVariantsConfig.getVariantRecordLookupFunction() == null)
|
||||
{
|
||||
for(Map.Entry<BackendVariantSetting, String> entry : backendSettingSourceFieldNameMap.entrySet())
|
||||
{
|
||||
assertCondition(optionsTable.getFields().containsKey(entry.getValue()), "Unrecognized fieldName [" + entry.getValue() + "] in backendSettingSourceFieldNameMap in backendVariantsConfig in [" + backendName + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(backendVariantsConfig.getVariantRecordLookupFunction() != null)
|
||||
{
|
||||
validateSimpleCodeReference("VariantRecordSupplier in backendVariantsConfig in backend [" + backendName + "]: ", backendVariantsConfig.getVariantRecordLookupFunction(), UnsafeFunction.class, Function.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
assertCondition(backendVariantsConfig == null, "Should not have a backendVariantsConfig in backend [" + backendName + "] which is not marked as usesVariants");
|
||||
}
|
||||
|
||||
///////////////////////////////////////////
|
||||
// let the backend do its own validation //
|
||||
///////////////////////////////////////////
|
||||
backend.performValidation(this);
|
||||
|
||||
runPlugins(QBackendMetaData.class, backend, qInstance);
|
||||
@ -559,13 +645,15 @@ public class QInstanceValidator
|
||||
private void validateAuthentication(QInstance qInstance)
|
||||
{
|
||||
QAuthenticationMetaData authentication = qInstance.getAuthentication();
|
||||
if(authentication != null)
|
||||
if(assertCondition(authentication != null, "Authentication MetaData must be defined."))
|
||||
{
|
||||
if(authentication.getCustomizer() != null)
|
||||
{
|
||||
validateSimpleCodeReference("Instance Authentication meta data customizer ", authentication.getCustomizer(), QAuthenticationModuleCustomizerInterface.class);
|
||||
}
|
||||
|
||||
authentication.validate(qInstance, this);
|
||||
|
||||
runPlugins(QAuthenticationMetaData.class, authentication, qInstance);
|
||||
}
|
||||
}
|
||||
@ -762,15 +850,38 @@ public class QInstanceValidator
|
||||
{
|
||||
if(assertCondition(StringUtils.hasContent(association.getName()), "missing a name for an Association on table " + table.getName()))
|
||||
{
|
||||
String messageSuffix = " for Association " + association.getName() + " on table " + table.getName();
|
||||
String messageSuffix = " for Association " + association.getName() + " on table " + table.getName();
|
||||
boolean recognizedTable = false;
|
||||
if(assertCondition(StringUtils.hasContent(association.getAssociatedTableName()), "missing associatedTableName" + messageSuffix))
|
||||
{
|
||||
assertCondition(qInstance.getTable(association.getAssociatedTableName()) != null, "unrecognized associatedTableName " + association.getAssociatedTableName() + messageSuffix);
|
||||
if(assertCondition(qInstance.getTable(association.getAssociatedTableName()) != null, "unrecognized associatedTableName " + association.getAssociatedTableName() + messageSuffix))
|
||||
{
|
||||
recognizedTable = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(assertCondition(StringUtils.hasContent(association.getJoinName()), "missing joinName" + messageSuffix))
|
||||
{
|
||||
assertCondition(qInstance.getJoin(association.getJoinName()) != null, "unrecognized joinName " + association.getJoinName() + messageSuffix);
|
||||
QJoinMetaData join = qInstance.getJoin(association.getJoinName());
|
||||
if(assertCondition(join != null, "unrecognized joinName " + association.getJoinName() + messageSuffix))
|
||||
{
|
||||
assert join != null; // covered by the assertCondition
|
||||
|
||||
if(recognizedTable)
|
||||
{
|
||||
boolean isLeftToRight = join.getLeftTable().equals(table.getName()) && join.getRightTable().equals(association.getAssociatedTableName());
|
||||
boolean isRightToLeft = join.getRightTable().equals(table.getName()) && join.getLeftTable().equals(association.getAssociatedTableName());
|
||||
assertCondition(isLeftToRight || isRightToLeft, "join [" + association.getJoinName() + "] does not connect tables [" + table.getName() + "] and [" + association.getAssociatedTableName() + "]" + messageSuffix);
|
||||
if(isLeftToRight)
|
||||
{
|
||||
assertCondition(join.getType().equals(JoinType.ONE_TO_MANY) || join.getType().equals(JoinType.ONE_TO_ONE), "Join type does not have 'one' on this table's side side (left)" + messageSuffix);
|
||||
}
|
||||
else if(isRightToLeft)
|
||||
{
|
||||
assertCondition(join.getType().equals(JoinType.MANY_TO_ONE) || join.getType().equals(JoinType.ONE_TO_ONE), "Join type does not have 'one' on this table's side (right)" + messageSuffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -922,18 +1033,13 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private <T extends FieldBehavior<T>> void validateTableField(QInstance qInstance, String tableName, String fieldName, QTableMetaData table, QFieldMetaData field)
|
||||
private void validateTableField(QInstance qInstance, String tableName, String fieldName, QTableMetaData table, QFieldMetaData field)
|
||||
{
|
||||
assertCondition(Objects.equals(fieldName, field.getName()),
|
||||
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
|
||||
|
||||
if(field.getPossibleValueSourceName() != null)
|
||||
{
|
||||
assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
|
||||
"Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + ".");
|
||||
}
|
||||
|
||||
String prefix = "Field " + fieldName + " in table " + tableName + " ";
|
||||
validateFieldPossibleValueSourceAttributes(qInstance, field, prefix);
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// validate things we know about field behaviors //
|
||||
@ -944,14 +1050,23 @@ public class QInstanceValidator
|
||||
assertCondition(field.getMaxLength() != null, prefix + "specifies a ValueTooLongBehavior, but not a maxLength.");
|
||||
}
|
||||
|
||||
Set<Class<FieldBehavior<T>>> usedFieldBehaviorTypes = new HashSet<>();
|
||||
Set<Class<FieldBehavior<?>>> usedFieldBehaviorTypes = new HashSet<>();
|
||||
if(field.getBehaviors() != null)
|
||||
{
|
||||
for(FieldBehavior<?> fieldBehavior : field.getBehaviors())
|
||||
{
|
||||
Class<FieldBehavior<T>> behaviorClass = (Class<FieldBehavior<T>>) fieldBehavior.getClass();
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<FieldBehavior<?>> behaviorClass = (Class<FieldBehavior<?>>) fieldBehavior.getClass();
|
||||
|
||||
errors.addAll(fieldBehavior.validateBehaviorConfiguration(table, field));
|
||||
List<String> behaviorErrors = fieldBehavior.validateBehaviorConfiguration(table, field);
|
||||
if(behaviorErrors != null)
|
||||
{
|
||||
String prefixMinusTrailingSpace = prefix.replaceFirst(" *$", "");
|
||||
for(String behaviorError : behaviorErrors)
|
||||
{
|
||||
errors.add(prefixMinusTrailingSpace + ": " + behaviorClass.getSimpleName() + ": " + behaviorError);
|
||||
}
|
||||
}
|
||||
|
||||
if(!fieldBehavior.allowMultipleBehaviorsOfThisType())
|
||||
{
|
||||
@ -1037,6 +1152,31 @@ public class QInstanceValidator
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void validateFieldPossibleValueSourceAttributes(QInstance qInstance, QFieldMetaData field, String prefix)
|
||||
{
|
||||
if(field.getPossibleValueSourceName() != null)
|
||||
{
|
||||
assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
|
||||
prefix + "has an unrecognized possibleValueSourceName " + field.getPossibleValueSourceName());
|
||||
|
||||
assertCondition(field.getInlinePossibleValueSource() == null, prefix.trim() + " has both a possibleValueSourceName and an inlinePossibleValueSource, which is not allowed.");
|
||||
}
|
||||
|
||||
if(field.getInlinePossibleValueSource() != null)
|
||||
{
|
||||
String name = "inlinePossibleValueSource for " + prefix.trim();
|
||||
if(assertCondition(QPossibleValueSourceType.ENUM.equals(field.getInlinePossibleValueSource().getType()), name + " must have a type of ENUM."))
|
||||
{
|
||||
validatePossibleValueSource(qInstance, name, field.getInlinePossibleValueSource());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -1269,7 +1409,7 @@ public class QInstanceValidator
|
||||
//////////////////////////////////////////////////
|
||||
// make sure the customizer can be instantiated //
|
||||
//////////////////////////////////////////////////
|
||||
Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass);
|
||||
Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass, codeReference);
|
||||
|
||||
TableCustomizers tableCustomizer = TableCustomizers.forRole(roleName);
|
||||
if(tableCustomizer == null)
|
||||
@ -1286,7 +1426,7 @@ public class QInstanceValidator
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
if(customizerInstance != null && tableCustomizer.getExpectedType() != null)
|
||||
{
|
||||
assertObjectCanBeCasted(prefix, tableCustomizer.getExpectedType(), customizerInstance);
|
||||
assertObjectCanBeCasted(prefix, customizerInstance, tableCustomizer.getExpectedType());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1298,18 +1438,31 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
** Make sure that a given object can be casted to an expected type.
|
||||
*******************************************************************************/
|
||||
private <T> T assertObjectCanBeCasted(String errorPrefix, Class<T> expectedType, Object object)
|
||||
private void assertObjectCanBeCasted(String errorPrefix, Object object, Class<?>... anyOfExpectedClasses)
|
||||
{
|
||||
T castedObject = null;
|
||||
try
|
||||
for(Class<?> expectedClass : anyOfExpectedClasses)
|
||||
{
|
||||
castedObject = expectedType.cast(object);
|
||||
try
|
||||
{
|
||||
expectedClass.cast(object);
|
||||
return;
|
||||
}
|
||||
catch(ClassCastException e)
|
||||
{
|
||||
/////////////////////////////////////
|
||||
// try next type (if there is one) //
|
||||
/////////////////////////////////////
|
||||
}
|
||||
}
|
||||
catch(ClassCastException e)
|
||||
|
||||
if(anyOfExpectedClasses.length == 1)
|
||||
{
|
||||
errors.add(errorPrefix + "CodeReference is not of the expected type: " + expectedType);
|
||||
errors.add(errorPrefix + "CodeReference is not of the expected type: " + anyOfExpectedClasses[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.add(errorPrefix + "CodeReference is not any of the expected types: " + Arrays.stream(anyOfExpectedClasses).map(c -> c.getName()).collect(Collectors.joining(", ")));
|
||||
}
|
||||
return castedObject;
|
||||
}
|
||||
|
||||
|
||||
@ -1317,8 +1470,13 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Object getInstanceOfCodeReference(String prefix, Class<?> clazz)
|
||||
private Object getInstanceOfCodeReference(String prefix, Class<?> clazz, QCodeReference codeReference)
|
||||
{
|
||||
if(codeReference instanceof QCodeReferenceLambda<?> lambdaCodeReference)
|
||||
{
|
||||
return (lambdaCodeReference.getLambda());
|
||||
}
|
||||
|
||||
Object instance = null;
|
||||
try
|
||||
{
|
||||
@ -1497,21 +1655,26 @@ public class QInstanceValidator
|
||||
Set<String> usedStepNames = new HashSet<>();
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(process.getStepList()), "At least 1 step must be defined in process " + processName + "."))
|
||||
{
|
||||
int index = 0;
|
||||
int index = -1;
|
||||
for(QStepMetaData step : process.getStepList())
|
||||
{
|
||||
index++;
|
||||
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++;
|
||||
|
||||
////////////////////////////////////////////
|
||||
// validate instantiation of step classes //
|
||||
////////////////////////////////////////////
|
||||
if(step instanceof QBackendStepMetaData backendStepMetaData)
|
||||
{
|
||||
if(assertCondition(backendStepMetaData.getCode() != null, "Missing code for a backend step at index " + index + " in process " + processName))
|
||||
{
|
||||
validateSimpleCodeReference("Process " + processName + ", backend step at index " + index + ", code reference: ", backendStepMetaData.getCode(), BackendStep.class);
|
||||
}
|
||||
|
||||
if(backendStepMetaData.getInputMetaData() != null && CollectionUtils.nullSafeHasContents(backendStepMetaData.getInputMetaData().getFieldList()))
|
||||
{
|
||||
for(QFieldMetaData fieldMetaData : backendStepMetaData.getInputMetaData().getFieldList())
|
||||
@ -1544,6 +1707,16 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
|
||||
for(QFieldMetaData field : process.getInputFields())
|
||||
{
|
||||
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", input field " + field.getName() + " ");
|
||||
}
|
||||
|
||||
for(QFieldMetaData field : process.getOutputFields())
|
||||
{
|
||||
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", output field " + field.getName() + " ");
|
||||
}
|
||||
|
||||
if(process.getCancelStep() != null)
|
||||
{
|
||||
if(assertCondition(process.getCancelStep().getCode() != null, "Cancel step is missing a code reference, in process " + processName))
|
||||
@ -1659,9 +1832,12 @@ public class QInstanceValidator
|
||||
|
||||
String dataSourceErrorPrefix = "Report " + reportName + " data source " + dataSource.getName() + " ";
|
||||
|
||||
boolean hasASource = false;
|
||||
|
||||
if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
{
|
||||
assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (exactly 1 is required).");
|
||||
hasASource = true;
|
||||
assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (not compatible together).");
|
||||
if(assertCondition(qInstance.getTable(dataSource.getSourceTable()) != null, dataSourceErrorPrefix + "source table " + dataSource.getSourceTable() + " is not a table in this instance."))
|
||||
{
|
||||
if(dataSource.getQueryFilter() != null)
|
||||
@ -1670,14 +1846,21 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(dataSource.getStaticDataSupplier() != null)
|
||||
|
||||
if(dataSource.getStaticDataSupplier() != null)
|
||||
{
|
||||
assertCondition(dataSource.getCustomRecordSource() == null, dataSourceErrorPrefix + "has both a staticDataSupplier and a customRecordSource (not compatible together).");
|
||||
hasASource = true;
|
||||
validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getStaticDataSupplier(), Supplier.class);
|
||||
}
|
||||
else
|
||||
|
||||
if(dataSource.getCustomRecordSource() != null)
|
||||
{
|
||||
errors.add(dataSourceErrorPrefix + "does not have a sourceTable or a staticDataSupplier (exactly 1 is required).");
|
||||
hasASource = true;
|
||||
validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getCustomRecordSource(), ReportCustomRecordSourceInterface.class);
|
||||
}
|
||||
|
||||
assertCondition(hasASource, dataSourceErrorPrefix + "does not have a sourceTable, customRecordSource, or a staticDataSupplier.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1742,7 +1925,7 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateQueryFilter(QInstance qInstance, String context, QTableMetaData table, QQueryFilter queryFilter, List<QueryJoin> queryJoins)
|
||||
public void validateQueryFilter(QInstance qInstance, String context, QTableMetaData table, QQueryFilter queryFilter, List<QueryJoin> queryJoins)
|
||||
{
|
||||
for(QFilterCriteria criterion : CollectionUtils.nonNullList(queryFilter.getCriteria()))
|
||||
{
|
||||
@ -1786,7 +1969,8 @@ public class QInstanceValidator
|
||||
{
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1);
|
||||
String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1);
|
||||
String tableNameBeforeDot = fieldName.substring(0, fieldName.lastIndexOf("."));
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(queryJoins))
|
||||
{
|
||||
@ -1810,11 +1994,32 @@ public class QInstanceValidator
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.add("QInstanceValidator does not yet support finding a field that looks like a join field, but isn't associated with a query.");
|
||||
return (true);
|
||||
// todo! for(QJoinMetaData join : CollectionUtils.nonNullMap(qInstance.getJoins()).values())
|
||||
// {
|
||||
// }
|
||||
if(this.joinGraph != null)
|
||||
{
|
||||
Set<JoinGraph.JoinConnectionList> joinConnections = joinGraph.getJoinConnections(table.getName());
|
||||
for(JoinGraph.JoinConnectionList joinConnectionList : joinConnections)
|
||||
{
|
||||
JoinGraph.JoinConnection joinConnection = joinConnectionList.list().get(joinConnectionList.list().size() - 1);
|
||||
if(tableNameBeforeDot.equals(joinConnection.joinTable()))
|
||||
{
|
||||
QTableMetaData joinTable = qInstance.getTable(tableNameBeforeDot);
|
||||
if(joinTable.getFields().containsKey(fieldNameAfterDot))
|
||||
{
|
||||
/////////////////////////
|
||||
// mmm, looks valid... //
|
||||
/////////////////////////
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - not sure how vulnerable we are to ongoing issues here... //
|
||||
// idea: let a filter (or any object?) be opted out of validation, some version of //
|
||||
// a static map of objects we can check at the top of various validate methods... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
errors.add("Failed to find field named: " + fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1918,6 +2123,11 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
|
||||
if(widget.getValidatorPlugin() != null)
|
||||
{
|
||||
widget.getValidatorPlugin().validate(widget, qInstance, this);
|
||||
}
|
||||
|
||||
runPlugins(QWidgetMetaDataInterface.class, widget, qInstance);
|
||||
}
|
||||
);
|
||||
@ -1936,78 +2146,90 @@ public class QInstanceValidator
|
||||
qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) ->
|
||||
{
|
||||
assertCondition(Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + ".");
|
||||
if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName))
|
||||
validatePossibleValueSource(qInstance, pvsName, possibleValueSource);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void validatePossibleValueSource(QInstance qInstance, String name, QPossibleValueSource possibleValueSource)
|
||||
{
|
||||
if(assertCondition(possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + name))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// assert about fields that should and should not be set, based on possible value source type //
|
||||
// do additional type-specific validations as well //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
switch(possibleValueSource.getType())
|
||||
{
|
||||
case ENUM ->
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// assert about fields that should and should not be set, based on possible value source type //
|
||||
// do additional type-specific validations as well //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
switch(possibleValueSource.getType())
|
||||
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + name + " should not have a tableName.");
|
||||
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "enum-type possibleValueSource " + name + " should not have searchFields.");
|
||||
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "enum-type possibleValueSource " + name + " should not have orderByFields.");
|
||||
assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + name + " should not have a customCodeReference.");
|
||||
|
||||
assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + name + " is missing enum values");
|
||||
}
|
||||
case TABLE ->
|
||||
{
|
||||
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + name + " should not have enum values.");
|
||||
assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + name + " should not have a customCodeReference.");
|
||||
|
||||
QTableMetaData tableMetaData = null;
|
||||
if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + name + " is missing a tableName."))
|
||||
{
|
||||
case ENUM ->
|
||||
{
|
||||
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "enum-type possibleValueSource " + pvsName + " should not have a tableName.");
|
||||
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "enum-type possibleValueSource " + pvsName + " should not have searchFields.");
|
||||
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "enum-type possibleValueSource " + pvsName + " should not have orderByFields.");
|
||||
assertCondition(possibleValueSource.getCustomCodeReference() == null, "enum-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
|
||||
|
||||
assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()), "enum-type possibleValueSource " + pvsName + " is missing enum values");
|
||||
}
|
||||
case TABLE ->
|
||||
{
|
||||
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "table-type possibleValueSource " + pvsName + " should not have enum values.");
|
||||
assertCondition(possibleValueSource.getCustomCodeReference() == null, "table-type possibleValueSource " + pvsName + " should not have a customCodeReference.");
|
||||
|
||||
QTableMetaData tableMetaData = null;
|
||||
if(assertCondition(StringUtils.hasContent(possibleValueSource.getTableName()), "table-type possibleValueSource " + pvsName + " is missing a tableName."))
|
||||
{
|
||||
tableMetaData = qInstance.getTable(possibleValueSource.getTableName());
|
||||
assertCondition(tableMetaData != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + pvsName + ".");
|
||||
}
|
||||
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "table-type possibleValueSource " + pvsName + " is missing searchFields."))
|
||||
{
|
||||
if(tableMetaData != null)
|
||||
{
|
||||
QTableMetaData finalTableMetaData = tableMetaData;
|
||||
for(String searchField : possibleValueSource.getSearchFields())
|
||||
{
|
||||
assertNoException(() -> finalTableMetaData.getField(searchField), "possibleValueSource " + pvsName + " has an unrecognized searchField: " + searchField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "table-type possibleValueSource " + pvsName + " is missing orderByFields."))
|
||||
{
|
||||
if(tableMetaData != null)
|
||||
{
|
||||
QTableMetaData finalTableMetaData = tableMetaData;
|
||||
|
||||
for(QFilterOrderBy orderByField : possibleValueSource.getOrderByFields())
|
||||
{
|
||||
assertNoException(() -> finalTableMetaData.getField(orderByField.getFieldName()), "possibleValueSource " + pvsName + " has an unrecognized orderByField: " + orderByField.getFieldName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case CUSTOM ->
|
||||
{
|
||||
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + pvsName + " should not have enum values.");
|
||||
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + pvsName + " should not have a tableName.");
|
||||
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "custom-type possibleValueSource " + pvsName + " should not have searchFields.");
|
||||
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "custom-type possibleValueSource " + pvsName + " should not have orderByFields.");
|
||||
|
||||
if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference."))
|
||||
{
|
||||
validateSimpleCodeReference("PossibleValueSource " + pvsName + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class);
|
||||
}
|
||||
}
|
||||
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
|
||||
tableMetaData = qInstance.getTable(possibleValueSource.getTableName());
|
||||
assertCondition(tableMetaData != null, "Unrecognized table " + possibleValueSource.getTableName() + " for possibleValueSource " + name + ".");
|
||||
}
|
||||
|
||||
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "table-type possibleValueSource " + name + " is missing searchFields."))
|
||||
{
|
||||
if(tableMetaData != null)
|
||||
{
|
||||
QTableMetaData finalTableMetaData = tableMetaData;
|
||||
for(String searchField : possibleValueSource.getSearchFields())
|
||||
{
|
||||
assertNoException(() -> finalTableMetaData.getField(searchField), "possibleValueSource " + name + " has an unrecognized searchField: " + searchField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "table-type possibleValueSource " + name + " is missing orderByFields."))
|
||||
{
|
||||
if(tableMetaData != null)
|
||||
{
|
||||
QTableMetaData finalTableMetaData = tableMetaData;
|
||||
|
||||
for(QFilterOrderBy orderByField : possibleValueSource.getOrderByFields())
|
||||
{
|
||||
assertNoException(() -> finalTableMetaData.getField(orderByField.getFieldName()), "possibleValueSource " + name + " has an unrecognized orderByField: " + orderByField.getFieldName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
case CUSTOM ->
|
||||
{
|
||||
assertCondition(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getEnumValues()), "custom-type possibleValueSource " + name + " should not have enum values.");
|
||||
assertCondition(!StringUtils.hasContent(possibleValueSource.getTableName()), "custom-type possibleValueSource " + name + " should not have a tableName.");
|
||||
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getSearchFields()), "custom-type possibleValueSource " + name + " should not have searchFields.");
|
||||
assertCondition(!CollectionUtils.nullSafeHasContents(possibleValueSource.getOrderByFields()), "custom-type possibleValueSource " + name + " should not have orderByFields.");
|
||||
|
||||
if(assertCondition(possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + name + " is missing a customCodeReference."))
|
||||
{
|
||||
validateSimpleCodeReference("PossibleValueSource " + name + " custom code reference: ", possibleValueSource.getCustomCodeReference(), QCustomPossibleValueProvider.class);
|
||||
}
|
||||
}
|
||||
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
|
||||
}
|
||||
|
||||
assertCondition(possibleValueSource.getIdType() != null, "possibleValueSource " + name + " is missing its idType.");
|
||||
|
||||
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2016,7 +2238,8 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?> expectedClass)
|
||||
@SafeVarargs
|
||||
private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?>... anyOfExpectedClasses)
|
||||
{
|
||||
if(!preAssertionsForCodeReference(codeReference, prefix))
|
||||
{
|
||||
@ -2037,14 +2260,14 @@ public class QInstanceValidator
|
||||
//////////////////////////////////////////////////
|
||||
// make sure the customizer can be instantiated //
|
||||
//////////////////////////////////////////////////
|
||||
Object classInstance = getInstanceOfCodeReference(prefix, clazz);
|
||||
Object classInstance = getInstanceOfCodeReference(prefix, clazz, codeReference);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// make sure the customizer instance can be cast to the expected type //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
if(classInstance != null)
|
||||
{
|
||||
assertObjectCanBeCasted(prefix, expectedClass, classInstance);
|
||||
assertObjectCanBeCasted(prefix, classInstance, anyOfExpectedClasses);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2060,6 +2283,11 @@ public class QInstanceValidator
|
||||
Class<?> clazz = null;
|
||||
try
|
||||
{
|
||||
if(codeReference instanceof QCodeReferenceLambda<?> lambdaCodeReference)
|
||||
{
|
||||
return (lambdaCodeReference.getLambda().getClass());
|
||||
}
|
||||
|
||||
clazz = Class.forName(codeReference.getName());
|
||||
}
|
||||
catch(ClassNotFoundException e)
|
||||
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.instances;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
@ -71,6 +72,18 @@ public class SecretsManagerUtils
|
||||
** and write them to a .env file (backing up any pre-existing .env files first).
|
||||
*******************************************************************************/
|
||||
public static void writeEnvFromSecretsWithNamePrefix(String prefix) throws IOException
|
||||
{
|
||||
writeEnvFromSecretsWithNamePrefix(prefix, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** IF secret manager ENV vars are set,
|
||||
** THEN lookup all secrets starting with the given prefix,
|
||||
** and write them to a .env file (backing up any pre-existing .env files first).
|
||||
*******************************************************************************/
|
||||
public static void writeEnvFromSecretsWithNamePrefix(String prefix, boolean quoteValues) throws IOException
|
||||
{
|
||||
Optional<AWSSecretsManager> optionalSecretsManagerClient = getSecretsManagerClient();
|
||||
if(optionalSecretsManagerClient.isPresent())
|
||||
@ -90,7 +103,9 @@ public class SecretsManagerUtils
|
||||
Optional<String> secretValue = getSecret(prefix, nameWithoutPrefix);
|
||||
if(secretValue.isPresent())
|
||||
{
|
||||
String envLine = nameWithoutPrefix + "=" + secretValue.get();
|
||||
String envLine = quoteValues
|
||||
? nameWithoutPrefix + "=\"" + secretValue.get() + "\""
|
||||
: nameWithoutPrefix + "=" + secretValue.get();
|
||||
fullEnv.append(envLine).append('\n');
|
||||
}
|
||||
}
|
||||
@ -113,7 +128,7 @@ public class SecretsManagerUtils
|
||||
dotEnv.renameTo(new File(".env.backup-" + System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
FileUtils.writeStringToFile(dotEnv, fullEnv.toString());
|
||||
FileUtils.writeStringToFile(dotEnv, fullEnv.toString(), StandardCharsets.UTF_8);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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.enrichment.plugins;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Interface for additional / optional enrichment to be done on q instance members.
|
||||
** Some may be provided by QQQ - others can be defined by applications.
|
||||
*******************************************************************************/
|
||||
public interface QInstanceEnricherPluginInterface<T>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
void enrich(T object, QInstance qInstance);
|
||||
|
||||
}
|
@ -0,0 +1,510 @@
|
||||
/*
|
||||
* 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.loaders;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.YamlUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import static com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsInteger;
|
||||
import static com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsString;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Abstract base class in hierarchy of classes that know how to construct &
|
||||
** populate QMetaDataObject instances, based on input streams (e.g., from files).
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractMetaDataLoader<T extends QMetaDataObject>
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(AbstractMetaDataLoader.class);
|
||||
|
||||
private String fileName;
|
||||
|
||||
private List<LoadingProblem> problems = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public T fileToMetaDataObject(QInstance qInstance, InputStream inputStream, String fileName) throws QMetaDataLoaderException
|
||||
{
|
||||
this.fileName = fileName;
|
||||
Map<String, Object> map = fileToMap(inputStream, fileName);
|
||||
LoadingContext loadingContext = new LoadingContext(fileName, "/");
|
||||
return (mapToMetaDataObject(qInstance, map, loadingContext));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public abstract T mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected Map<String, Object> fileToMap(InputStream inputStream, String fileName) throws QMetaDataLoaderException
|
||||
{
|
||||
try
|
||||
{
|
||||
String string = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
|
||||
string = StringUtils.ltrim(string);
|
||||
if(fileName.toLowerCase().endsWith(".json"))
|
||||
{
|
||||
return JsonUtils.toObject(string, new TypeReference<>() {});
|
||||
}
|
||||
else if(fileName.toLowerCase().endsWith(".yaml") || fileName.toLowerCase().endsWith(".yml"))
|
||||
{
|
||||
return YamlUtils.toMap(string);
|
||||
}
|
||||
|
||||
throw (new QMetaDataLoaderException("Unsupported file format (based on file name: " + fileName + ")"));
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
throw new QMetaDataLoaderException("Error building map from file: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
protected void reflectivelyMap(QInstance qInstance, QMetaDataObject targetObject, Map<String, Object> map, LoadingContext context)
|
||||
{
|
||||
Class<? extends QMetaDataObject> targetClass = targetObject.getClass();
|
||||
Set<String> usedFieldNames = new HashSet<>();
|
||||
|
||||
for(Method method : targetClass.getMethods())
|
||||
{
|
||||
try
|
||||
{
|
||||
if(method.getName().startsWith("set") && method.getParameterTypes().length == 1)
|
||||
{
|
||||
String propertyName = StringUtils.lcFirst(method.getName().substring(3));
|
||||
|
||||
if(map.containsKey(propertyName))
|
||||
{
|
||||
usedFieldNames.add(propertyName);
|
||||
Class<?> parameterType = method.getParameterTypes()[0];
|
||||
Object rawValue = map.get(propertyName);
|
||||
|
||||
try
|
||||
{
|
||||
Object mappedValue = reflectivelyMapValue(qInstance, method, parameterType, rawValue, context.descendToProperty(propertyName));
|
||||
method.invoke(targetObject, mappedValue);
|
||||
}
|
||||
catch(NoValueException nve)
|
||||
{
|
||||
///////////////////////
|
||||
// don't call setter //
|
||||
///////////////////////
|
||||
LOG.debug("at " + context + ": No value was mapped for property [" + propertyName + "] on " + targetClass.getSimpleName() + "." + method.getName() + ", raw value: [" + rawValue + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
addProblem(new LoadingProblem(context, "Error reflectively mapping on " + targetClass.getName() + "." + method.getName(), e));
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
// mmm, slightly sus... //
|
||||
//////////////////////////
|
||||
map.remove("class");
|
||||
map.remove("version");
|
||||
|
||||
Set<String> unrecognizedKeys = new HashSet<>(map.keySet());
|
||||
unrecognizedKeys.removeAll(usedFieldNames);
|
||||
|
||||
if(!unrecognizedKeys.isEmpty())
|
||||
{
|
||||
addProblem(new LoadingProblem(context, unrecognizedKeys.size() + " Unrecognized " + StringUtils.plural(unrecognizedKeys, "property", "properties") + ": " + unrecognizedKeys));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public Object reflectivelyMapValue(QInstance qInstance, Method method, Class<?> parameterType, Object rawValue, LoadingContext context) throws Exception
|
||||
{
|
||||
if(rawValue instanceof String s && s.matches("^\\$\\{.+\\..+}"))
|
||||
{
|
||||
rawValue = new QMetaDataVariableInterpreter().interpret(s);
|
||||
LOG.debug("Interpreted raw value [" + s + "] as [" + StringUtils.maskAndTruncate(ValueUtils.getValueAsString(rawValue) + "]"));
|
||||
}
|
||||
|
||||
if(parameterType.equals(String.class))
|
||||
{
|
||||
return (getValueAsString(rawValue));
|
||||
}
|
||||
else if(parameterType.equals(Integer.class))
|
||||
{
|
||||
try
|
||||
{
|
||||
return (getValueAsInteger(rawValue));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
addProblem(new LoadingProblem(context, "[" + rawValue + "] is not an Integer value."));
|
||||
}
|
||||
}
|
||||
else if(parameterType.equals(Boolean.class))
|
||||
{
|
||||
if("true".equals(rawValue) || Boolean.TRUE.equals(rawValue))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
else if("false".equals(rawValue) || Boolean.FALSE.equals(rawValue))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
else if(rawValue == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
else
|
||||
{
|
||||
addProblem(new LoadingProblem(context, "[" + rawValue + "] is not a boolean value (must be 'true' or 'false')."));
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
else if(parameterType.equals(boolean.class))
|
||||
{
|
||||
if("true".equals(rawValue) || Boolean.TRUE.equals(rawValue))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
else if("false".equals(rawValue) || Boolean.FALSE.equals(rawValue))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
else
|
||||
{
|
||||
addProblem(new LoadingProblem(context, rawValue + " is not a boolean value (must be 'true' or 'false')."));
|
||||
throw (new NoValueException());
|
||||
}
|
||||
}
|
||||
else if(parameterType.equals(List.class))
|
||||
{
|
||||
Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
|
||||
Class<?> actualTypeClass = Class.forName(actualTypeArgument.getTypeName());
|
||||
|
||||
if(rawValue instanceof @SuppressWarnings("rawtypes")List valueList)
|
||||
{
|
||||
List<Object> mappedValueList = new ArrayList<>();
|
||||
for(Object o : valueList)
|
||||
{
|
||||
try
|
||||
{
|
||||
Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, o, context);
|
||||
mappedValueList.add(mappedValue);
|
||||
}
|
||||
catch(NoValueException nve)
|
||||
{
|
||||
// leave off list
|
||||
}
|
||||
}
|
||||
return (mappedValueList);
|
||||
}
|
||||
}
|
||||
else if(parameterType.equals(Set.class))
|
||||
{
|
||||
Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
|
||||
Class<?> actualTypeClass = Class.forName(actualTypeArgument.getTypeName());
|
||||
|
||||
if(rawValue instanceof @SuppressWarnings("rawtypes")List valueList)
|
||||
{
|
||||
Set<Object> mappedValueSet = new LinkedHashSet<>();
|
||||
for(Object o : valueList)
|
||||
{
|
||||
try
|
||||
{
|
||||
Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, o, context);
|
||||
mappedValueSet.add(mappedValue);
|
||||
}
|
||||
catch(NoValueException nve)
|
||||
{
|
||||
// leave off list
|
||||
}
|
||||
}
|
||||
return (mappedValueSet);
|
||||
}
|
||||
}
|
||||
else if(parameterType.equals(Map.class))
|
||||
{
|
||||
Type keyType = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
|
||||
if(!keyType.equals(String.class))
|
||||
{
|
||||
addProblem(new LoadingProblem(context, "Unsupported key type for " + method + " got [" + keyType + "], expected [String]"));
|
||||
throw new NoValueException();
|
||||
}
|
||||
// todo make sure string
|
||||
|
||||
Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[1];
|
||||
Class<?> actualTypeClass = Class.forName(actualTypeArgument.getTypeName());
|
||||
|
||||
if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap)
|
||||
{
|
||||
Map<String, Object> mappedValueMap = new LinkedHashMap<>();
|
||||
for(Object o : valueMap.entrySet())
|
||||
{
|
||||
try
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
Map.Entry<String, Object> entry = (Map.Entry<String, Object>) o;
|
||||
Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, entry.getValue(), context);
|
||||
mappedValueMap.put(entry.getKey(), mappedValue);
|
||||
}
|
||||
catch(NoValueException nve)
|
||||
{
|
||||
// leave out of map
|
||||
}
|
||||
}
|
||||
return (mappedValueMap);
|
||||
}
|
||||
}
|
||||
else if(parameterType.isEnum())
|
||||
{
|
||||
String value = getValueAsString(rawValue);
|
||||
for(Object enumConstant : parameterType.getEnumConstants())
|
||||
{
|
||||
if(((Enum<?>) enumConstant).name().equals(value))
|
||||
{
|
||||
return (enumConstant);
|
||||
}
|
||||
}
|
||||
|
||||
addProblem(new LoadingProblem(context, "Unrecognized value [" + rawValue + "]. Expected one of: " + Arrays.toString(parameterType.getEnumConstants())));
|
||||
}
|
||||
else if(MetaDataLoaderRegistry.hasLoaderForClass(parameterType))
|
||||
{
|
||||
if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap)
|
||||
{
|
||||
Class<? extends AbstractMetaDataLoader<?>> loaderClass = MetaDataLoaderRegistry.getLoaderForClass(parameterType);
|
||||
AbstractMetaDataLoader<?> loader = loaderClass.getConstructor().newInstance();
|
||||
//noinspection unchecked
|
||||
return (loader.mapToMetaDataObject(qInstance, valueMap, context));
|
||||
}
|
||||
}
|
||||
else if(QMetaDataObject.class.isAssignableFrom(parameterType))
|
||||
{
|
||||
if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap)
|
||||
{
|
||||
QMetaDataObject childObject = (QMetaDataObject) parameterType.getConstructor().newInstance();
|
||||
//noinspection unchecked
|
||||
reflectivelyMap(qInstance, childObject, valueMap, context);
|
||||
return (childObject);
|
||||
}
|
||||
}
|
||||
else if(parameterType.equals(Serializable.class))
|
||||
{
|
||||
if(rawValue instanceof String
|
||||
|| rawValue instanceof Integer
|
||||
|| rawValue instanceof BigDecimal
|
||||
|| rawValue instanceof Boolean
|
||||
)
|
||||
{
|
||||
return rawValue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// todo clean up this message/level
|
||||
addProblem(new LoadingProblem(context, "No case for " + parameterType + " (arg to: " + method + ")"));
|
||||
}
|
||||
|
||||
throw new NoValueException();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// unclear if the below is needed. if so, useful to not re-write, but is hurting test coverage, so zombie until used //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
///***************************************************************************
|
||||
// *
|
||||
// ***************************************************************************/
|
||||
//protected ListOfMapOrMapOfMap getListOfMapOrMapOfMap(Map<String, Object> map, String key)
|
||||
//{
|
||||
// if(map.containsKey(key))
|
||||
// {
|
||||
// if(map.get(key) instanceof List)
|
||||
// {
|
||||
// return (new ListOfMapOrMapOfMap((List<Map<String, Object>>) map.get(key)));
|
||||
// }
|
||||
// else if(map.get(key) instanceof Map)
|
||||
// {
|
||||
// return (new ListOfMapOrMapOfMap((Map<String, Map<String, Object>>) map.get(key)));
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// LOG.warn("Expected list or map under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName()));
|
||||
// }
|
||||
// }
|
||||
|
||||
// return (null);
|
||||
//}
|
||||
|
||||
///***************************************************************************
|
||||
// *
|
||||
// ***************************************************************************/
|
||||
//protected List<Map<String, Object>> getListOfMap(Map<String, Object> map, String key)
|
||||
//{
|
||||
// if(map.containsKey(key))
|
||||
// {
|
||||
// if(map.get(key) instanceof List)
|
||||
// {
|
||||
// return (List<Map<String, Object>>) map.get(key);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// LOG.warn("Expected list under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName()));
|
||||
// }
|
||||
// }
|
||||
|
||||
// return (null);
|
||||
//}
|
||||
|
||||
///***************************************************************************
|
||||
// *
|
||||
// ***************************************************************************/
|
||||
//protected Map<String, Map<String, Object>> getMapOfMap(Map<String, Object> map, String key)
|
||||
//{
|
||||
// if(map.containsKey(key))
|
||||
// {
|
||||
// if(map.get(key) instanceof Map)
|
||||
// {
|
||||
// return (Map<String, Map<String, Object>>) map.get(key);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// LOG.warn("Expected map under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName()));
|
||||
// }
|
||||
// }
|
||||
|
||||
// return (null);
|
||||
//}
|
||||
|
||||
///***************************************************************************
|
||||
// **
|
||||
// ***************************************************************************/
|
||||
//protected record ListOfMapOrMapOfMap(List<Map<String, Object>> listOf, Map<String, Map<String, Object>> mapOf)
|
||||
//{
|
||||
// /*******************************************************************************
|
||||
// ** Constructor
|
||||
// **
|
||||
// *******************************************************************************/
|
||||
// public ListOfMapOrMapOfMap(List<Map<String, Object>> listOf)
|
||||
// {
|
||||
// this(listOf, null);
|
||||
// }
|
||||
|
||||
// /*******************************************************************************
|
||||
// ** Constructor
|
||||
// **
|
||||
// *******************************************************************************/
|
||||
// public ListOfMapOrMapOfMap(Map<String, Map<String, Object>> mapOf)
|
||||
// {
|
||||
// this(null, mapOf);
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fileName
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getFileName()
|
||||
{
|
||||
return fileName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static class NoValueException extends Exception
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public NoValueException()
|
||||
{
|
||||
super("No value");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public void addProblem(LoadingProblem problem)
|
||||
{
|
||||
problems.add(problem);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for problems
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<LoadingProblem> getProblems()
|
||||
{
|
||||
return (problems);
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.loaders;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.implementations.GenericMetaDataLoader;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
|
||||
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.memoization.AnyKey;
|
||||
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Generic implementation of AbstractMetaDataLoader, who "detects" the class
|
||||
** of meta data object to be created, then defers to an appropriate subclass
|
||||
** to do the work.
|
||||
*******************************************************************************/
|
||||
public class ClassDetectingMetaDataLoader extends AbstractMetaDataLoader<QMetaDataObject>
|
||||
{
|
||||
private static final Memoization<AnyKey, List<Class<?>>> memoizedMetaDataObjectClasses = new Memoization<>();
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public AbstractMetaDataLoader<?> getLoaderForFile(InputStream inputStream, String fileName) throws QMetaDataLoaderException
|
||||
{
|
||||
Map<String, Object> map = fileToMap(inputStream, fileName);
|
||||
return (getLoaderForMap(map));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public AbstractMetaDataLoader<?> getLoaderForMap(Map<String, Object> map) throws QMetaDataLoaderException
|
||||
{
|
||||
if(map.containsKey("class"))
|
||||
{
|
||||
String classProperty = ValueUtils.getValueAsString(map.get("class"));
|
||||
try
|
||||
{
|
||||
if(MetaDataLoaderRegistry.hasLoaderForSimpleName(classProperty))
|
||||
{
|
||||
Class<? extends AbstractMetaDataLoader<?>> loaderClass = MetaDataLoaderRegistry.getLoaderForSimpleName(classProperty);
|
||||
return (loaderClass.getConstructor().newInstance());
|
||||
}
|
||||
else
|
||||
{
|
||||
Optional<List<Class<?>>> metaDataClasses = memoizedMetaDataObjectClasses.getResult(AnyKey.getInstance(), k -> ClassPathUtils.getClassesContainingNameAndOfType("MetaData", QMetaDataObject.class));
|
||||
if(metaDataClasses.isEmpty())
|
||||
{
|
||||
throw (new QMetaDataLoaderException("Could not get list of metaDataObjects from class loader"));
|
||||
}
|
||||
|
||||
for(Class<?> c : metaDataClasses.get())
|
||||
{
|
||||
if(c.getSimpleName().equals(classProperty) && QMetaDataObject.class.isAssignableFrom(c))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<? extends QMetaDataObject> metaDataClass = (Class<? extends QMetaDataObject>) c;
|
||||
return new GenericMetaDataLoader<>(metaDataClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new QMetaDataLoaderException("Unexpected class [" + classProperty + "] (not a QMetaDataObject; doesn't have a registered MetaDataLoader) specified in " + getFileName());
|
||||
}
|
||||
catch(QMetaDataLoaderException qmdle)
|
||||
{
|
||||
throw (qmdle);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new QMetaDataLoaderException("Error handling class [" + classProperty + "] specified in " + getFileName(), e);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new QMetaDataLoaderException("Cannot detect meta-data type, because [class] attribute was not specified in file: " + getFileName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QMetaDataObject mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException
|
||||
{
|
||||
AbstractMetaDataLoader<?> loaderForMap = getLoaderForMap(map);
|
||||
return loaderForMap.mapToMetaDataObject(qInstance, map, context);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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.loaders;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Record to track where loader objects are - e.g., what file they're on,
|
||||
** and at what property path within the file (e.g., helps report problems).
|
||||
*******************************************************************************/
|
||||
public record LoadingContext(String fileName, String propertyPath)
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public LoadingContext descendToProperty(String propertyName)
|
||||
{
|
||||
return new LoadingContext(fileName, propertyPath + propertyName + "/");
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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.loaders;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** record that tracks a problem that was encountered when loading files.
|
||||
*******************************************************************************/
|
||||
public record LoadingProblem(LoadingContext context, String message, Exception exception) // todo Level if useful
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public LoadingProblem(LoadingContext context, String message)
|
||||
{
|
||||
this(context, message, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "at[" + context.fileName() + "][" + context.propertyPath() + "]: " + message;
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.loaders;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.Pair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** class that loads a directory full of meta data files into meta data objects,
|
||||
** and then sets all of them in a QInstance.
|
||||
*******************************************************************************/
|
||||
public class MetaDataLoaderHelper
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(MetaDataLoaderHelper.class);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public static void processAllMetaDataFilesInDirectory(QInstance qInstance, String path) throws QException
|
||||
{
|
||||
List<Pair<File, AbstractMetaDataLoader<?>>> loaders = new ArrayList<>();
|
||||
|
||||
File directory = new File(path);
|
||||
processAllMetaDataFilesInDirectory(loaders, directory);
|
||||
|
||||
// todo - some version of sorting the loaders by type or possibly a sort field within the files (or file names)
|
||||
|
||||
for(Pair<File, AbstractMetaDataLoader<?>> pair : loaders)
|
||||
{
|
||||
File file = pair.getA();
|
||||
AbstractMetaDataLoader<?> loader = pair.getB();
|
||||
try(FileInputStream fileInputStream = new FileInputStream(file))
|
||||
{
|
||||
QMetaDataObject qMetaDataObject = loader.fileToMetaDataObject(qInstance, fileInputStream, file.getName());
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(loader.getProblems()))
|
||||
{
|
||||
loader.getProblems().forEach(System.out::println);
|
||||
}
|
||||
|
||||
if(qMetaDataObject instanceof TopLevelMetaDataInterface topLevelMetaData)
|
||||
{
|
||||
topLevelMetaData.addSelfToInstance(qInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Received a non-topLevelMetaDataObject from file: " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error processing file: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
private static void processAllMetaDataFilesInDirectory(List<Pair<File, AbstractMetaDataLoader<?>>> loaders, File directory) throws QException
|
||||
{
|
||||
for(File file : Objects.requireNonNullElse(directory.listFiles(), new File[0]))
|
||||
{
|
||||
if(file.isDirectory())
|
||||
{
|
||||
processAllMetaDataFilesInDirectory(loaders, file);
|
||||
}
|
||||
else
|
||||
{
|
||||
try(FileInputStream fileInputStream = new FileInputStream(file))
|
||||
{
|
||||
AbstractMetaDataLoader<?> loader = new ClassDetectingMetaDataLoader().getLoaderForFile(fileInputStream, file.getName());
|
||||
loaders.add(Pair.of(file, loader));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error processing file: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. 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.loaders;
|
||||
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class MetaDataLoaderRegistry
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(AbstractMetaDataLoader.class);
|
||||
|
||||
private static final Map<Class<?>, Class<? extends AbstractMetaDataLoader<?>>> registeredLoaders = new HashMap<>();
|
||||
private static final Map<String, Class<? extends AbstractMetaDataLoader<?>>> registeredLoadersByTargetSimpleName = new HashMap<>();
|
||||
|
||||
static
|
||||
{
|
||||
try
|
||||
{
|
||||
List<Class<?>> classesInPackage = ClassPathUtils.getClassesInPackage(QTableMetaDataLoader.class.getPackageName());
|
||||
for(Class<?> possibleLoaderClass : classesInPackage)
|
||||
{
|
||||
try
|
||||
{
|
||||
Type superClass = possibleLoaderClass.getGenericSuperclass();
|
||||
if(superClass.getTypeName().startsWith(AbstractMetaDataLoader.class.getName() + "<"))
|
||||
{
|
||||
Type actualTypeArgument = ((ParameterizedType) superClass).getActualTypeArguments()[0];
|
||||
if(actualTypeArgument instanceof Class)
|
||||
{
|
||||
//noinspection unchecked
|
||||
Class<? extends AbstractMetaDataLoader<?>> loaderClass = (Class<? extends AbstractMetaDataLoader<?>>) possibleLoaderClass;
|
||||
|
||||
Class<?> metaDataObjectType = Class.forName(actualTypeArgument.getTypeName());
|
||||
registeredLoaders.put(metaDataObjectType, loaderClass);
|
||||
registeredLoadersByTargetSimpleName.put(metaDataObjectType.getSimpleName(), loaderClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.info("Error on class: " + possibleLoaderClass, e);
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("Registered loaders: " + registeredLoadersByTargetSimpleName);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error in static init block for MetaDataLoaderRegistry", e);
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static boolean hasLoaderForClass(Class<?> metaDataClass)
|
||||
{
|
||||
return registeredLoaders.containsKey(metaDataClass);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static Class<? extends AbstractMetaDataLoader<?>> getLoaderForClass(Class<?> metaDataClass)
|
||||
{
|
||||
return registeredLoaders.get(metaDataClass);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static boolean hasLoaderForSimpleName(String targetSimpleName)
|
||||
{
|
||||
return registeredLoadersByTargetSimpleName.containsKey(targetSimpleName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static Class<? extends AbstractMetaDataLoader<?>> getLoaderForSimpleName(String targetSimpleName)
|
||||
{
|
||||
return registeredLoadersByTargetSimpleName.get(targetSimpleName);
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.loaders;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class QMetaDataLoaderException extends Exception
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QMetaDataLoaderException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QMetaDataLoaderException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.loaders.implementations;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class GenericMetaDataLoader<T extends QMetaDataObject> extends AbstractMetaDataLoader<T>
|
||||
{
|
||||
private final Class<T> metaDataClass;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public GenericMetaDataLoader(Class<T> metaDataClass)
|
||||
{
|
||||
this.metaDataClass = metaDataClass;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public T mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException
|
||||
{
|
||||
try
|
||||
{
|
||||
T object = metaDataClass.getConstructor().newInstance();
|
||||
reflectivelyMap(qInstance, object, map, context);
|
||||
return (object);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QMetaDataLoaderException("Error loading metaData object of type " + metaDataClass.getSimpleName(), e));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.loaders.implementations;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class QStepDataLoader extends AbstractMetaDataLoader<QStepMetaData>
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QStepDataLoader.class);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QStepMetaData mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException
|
||||
{
|
||||
String stepType = ValueUtils.getValueAsString(map.get("stepType"));
|
||||
|
||||
if(!StringUtils.hasContent(stepType))
|
||||
{
|
||||
throw (new QMetaDataLoaderException("stepType was not specified for process step"));
|
||||
}
|
||||
|
||||
QStepMetaData step;
|
||||
if("backend".equalsIgnoreCase(stepType))
|
||||
{
|
||||
step = new QBackendStepMetaData();
|
||||
reflectivelyMap(qInstance, step, map, context);
|
||||
}
|
||||
else if("frontend".equalsIgnoreCase(stepType))
|
||||
{
|
||||
step = new QFrontendStepMetaData();
|
||||
reflectivelyMap(qInstance, step, map, context);
|
||||
}
|
||||
// todo - we have custom factory methods for this, so, maybe needs all custom loader?
|
||||
// else if("stateMachine".equalsIgnoreCase(stepType))
|
||||
// {
|
||||
// step = new QStateMachineStep();
|
||||
// reflectivelyMap(qInstance, step, map, context);
|
||||
// }
|
||||
else
|
||||
{
|
||||
throw (new QMetaDataLoaderException("Unsupported step stepType: " + stepType));
|
||||
}
|
||||
|
||||
return (step);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.loaders.implementations;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext;
|
||||
import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class QTableMetaDataLoader extends AbstractMetaDataLoader<QTableMetaData>
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QTableMetaDataLoader.class);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QTableMetaData mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException
|
||||
{
|
||||
QTableMetaData table = new QTableMetaData();
|
||||
|
||||
reflectivelyMap(qInstance, table, map, context);
|
||||
|
||||
// todo - handle QTableBackendDetails, based on backend's type
|
||||
|
||||
return (table);
|
||||
}
|
||||
|
||||
}
|
@ -280,6 +280,16 @@ public class QLogger
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void debug(LogPair... logPairs)
|
||||
{
|
||||
logger.warn(() -> makeJsonString(null, null, logPairs));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -420,6 +430,16 @@ public class QLogger
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void warn(LogPair... logPairs)
|
||||
{
|
||||
logger.warn(() -> makeJsonString(null, null, logPairs));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -480,6 +500,16 @@ public class QLogger
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void error(LogPair... logPairs)
|
||||
{
|
||||
logger.warn(() -> makeJsonString(null, null, logPairs));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -32,7 +32,6 @@ import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
|
||||
|
||||
@ -93,17 +92,6 @@ public class AbstractActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@JsonIgnore
|
||||
public QAuthenticationMetaData getAuthenticationMetaData()
|
||||
{
|
||||
return (getInstance().getAuthentication());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for instance
|
||||
**
|
||||
|
@ -152,5 +152,8 @@ public class AuditDetailAccumulator implements Serializable
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private record TableNameAndPrimaryKey(String tableName, Serializable primaryKey) {}
|
||||
}
|
||||
|
@ -326,6 +326,20 @@ public class AuditSingleInput implements Serializable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for details
|
||||
*******************************************************************************/
|
||||
public AuditSingleInput withDetailMessages(List<String> details)
|
||||
{
|
||||
for(String detail : details)
|
||||
{
|
||||
addDetail(message);
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -31,6 +31,16 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
*******************************************************************************/
|
||||
public class MetaDataInput extends AbstractActionInput
|
||||
{
|
||||
private String frontendName;
|
||||
private String frontendVersion;
|
||||
|
||||
private String middlewareName;
|
||||
private String middlewareVersion;
|
||||
|
||||
private String applicationName;
|
||||
private String applicationVersion;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -39,4 +49,190 @@ public class MetaDataInput extends AbstractActionInput
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for frontendName
|
||||
*******************************************************************************/
|
||||
public String getFrontendName()
|
||||
{
|
||||
return (this.frontendName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for frontendName
|
||||
*******************************************************************************/
|
||||
public void setFrontendName(String frontendName)
|
||||
{
|
||||
this.frontendName = frontendName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for frontendName
|
||||
*******************************************************************************/
|
||||
public MetaDataInput withFrontendName(String frontendName)
|
||||
{
|
||||
this.frontendName = frontendName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for frontendVersion
|
||||
*******************************************************************************/
|
||||
public String getFrontendVersion()
|
||||
{
|
||||
return (this.frontendVersion);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for frontendVersion
|
||||
*******************************************************************************/
|
||||
public void setFrontendVersion(String frontendVersion)
|
||||
{
|
||||
this.frontendVersion = frontendVersion;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for frontendVersion
|
||||
*******************************************************************************/
|
||||
public MetaDataInput withFrontendVersion(String frontendVersion)
|
||||
{
|
||||
this.frontendVersion = frontendVersion;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for middlewareName
|
||||
*******************************************************************************/
|
||||
public String getMiddlewareName()
|
||||
{
|
||||
return (this.middlewareName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for middlewareName
|
||||
*******************************************************************************/
|
||||
public void setMiddlewareName(String middlewareName)
|
||||
{
|
||||
this.middlewareName = middlewareName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for middlewareName
|
||||
*******************************************************************************/
|
||||
public MetaDataInput withMiddlewareName(String middlewareName)
|
||||
{
|
||||
this.middlewareName = middlewareName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for middlewareVersion
|
||||
*******************************************************************************/
|
||||
public String getMiddlewareVersion()
|
||||
{
|
||||
return (this.middlewareVersion);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for middlewareVersion
|
||||
*******************************************************************************/
|
||||
public void setMiddlewareVersion(String middlewareVersion)
|
||||
{
|
||||
this.middlewareVersion = middlewareVersion;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for middlewareVersion
|
||||
*******************************************************************************/
|
||||
public MetaDataInput withMiddlewareVersion(String middlewareVersion)
|
||||
{
|
||||
this.middlewareVersion = middlewareVersion;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for applicationName
|
||||
*******************************************************************************/
|
||||
public String getApplicationName()
|
||||
{
|
||||
return (this.applicationName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for applicationName
|
||||
*******************************************************************************/
|
||||
public void setApplicationName(String applicationName)
|
||||
{
|
||||
this.applicationName = applicationName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for applicationName
|
||||
*******************************************************************************/
|
||||
public MetaDataInput withApplicationName(String applicationName)
|
||||
{
|
||||
this.applicationName = applicationName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for applicationVersion
|
||||
*******************************************************************************/
|
||||
public String getApplicationVersion()
|
||||
{
|
||||
return (this.applicationVersion);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for applicationVersion
|
||||
*******************************************************************************/
|
||||
public void setApplicationVersion(String applicationVersion)
|
||||
{
|
||||
this.applicationVersion = applicationVersion;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for applicationVersion
|
||||
*******************************************************************************/
|
||||
public MetaDataInput withApplicationVersion(String applicationVersion)
|
||||
{
|
||||
this.applicationVersion = applicationVersion;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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.processes;
|
||||
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Object that stores adjustments that a process wants to make, at run-time,
|
||||
** to its meta-data.
|
||||
**
|
||||
** e.g., changing the steps; updating fields (e.g., changing an inline PVS,
|
||||
** or an isRequired attribute)
|
||||
*******************************************************************************/
|
||||
public class ProcessMetaDataAdjustment
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ProcessMetaDataAdjustment.class);
|
||||
|
||||
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
|
||||
private Map<String, QFieldMetaData> updatedFields = null;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ProcessMetaDataAdjustment withUpdatedField(QFieldMetaData field)
|
||||
{
|
||||
if(updatedFields == null)
|
||||
{
|
||||
updatedFields = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
if(!StringUtils.hasContent(field.getName()))
|
||||
{
|
||||
LOG.warn("Missing name on field in withUpdatedField - no update will happen.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if(updatedFields.containsKey(field.getName()))
|
||||
{
|
||||
LOG.info("UpdatedFields map already contained a field with this name - overwriting it.", logPair("fieldName", field.getName()));
|
||||
}
|
||||
|
||||
updatedFields.put(field.getName(), field);
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
|
||||
{
|
||||
return (this.updatedFrontendStepList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
{
|
||||
this.updatedFrontendStepList = updatedFrontendStepList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public ProcessMetaDataAdjustment withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
{
|
||||
this.updatedFrontendStepList = updatedFrontendStepList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for updatedFields
|
||||
*******************************************************************************/
|
||||
public Map<String, QFieldMetaData> getUpdatedFields()
|
||||
{
|
||||
return (this.updatedFields);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for updatedFields
|
||||
*******************************************************************************/
|
||||
public void setUpdatedFields(Map<String, QFieldMetaData> updatedFields)
|
||||
{
|
||||
this.updatedFields = updatedFields;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for updatedFields
|
||||
*******************************************************************************/
|
||||
public ProcessMetaDataAdjustment withUpdatedFields(Map<String, QFieldMetaData> updatedFields)
|
||||
{
|
||||
this.updatedFields = updatedFields;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -29,7 +29,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -41,11 +40,10 @@ public class ProcessState implements Serializable
|
||||
private Map<String, Serializable> values = new HashMap<>();
|
||||
private List<String> stepList = new ArrayList<>();
|
||||
private Optional<String> nextStepName = Optional.empty();
|
||||
private Optional<String> backStepName = Optional.empty();
|
||||
private boolean isStepBack = false;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// maybe, remove this altogether - just let the frontend compute & send if needed... but how does it know last version...? //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
|
||||
private ProcessMetaDataAdjustment processMetaDataAdjustment = null;
|
||||
|
||||
|
||||
|
||||
@ -126,6 +124,39 @@ public class ProcessState implements Serializable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for backStepName
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Optional<String> getBackStepName()
|
||||
{
|
||||
return backStepName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for backStepName
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setBackStepName(String backStepName)
|
||||
{
|
||||
this.backStepName = Optional.of(backStepName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** clear out the value of backStepName (set the Optional to empty)
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void clearBackStepName()
|
||||
{
|
||||
this.backStepName = Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for stepList
|
||||
**
|
||||
@ -148,33 +179,67 @@ public class ProcessState implements Serializable
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for updatedFrontendStepList
|
||||
** Getter for processMetaDataAdjustment
|
||||
*******************************************************************************/
|
||||
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
|
||||
public ProcessMetaDataAdjustment getProcessMetaDataAdjustment()
|
||||
{
|
||||
return (this.updatedFrontendStepList);
|
||||
return (this.processMetaDataAdjustment);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for updatedFrontendStepList
|
||||
** Setter for processMetaDataAdjustment
|
||||
*******************************************************************************/
|
||||
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
public void setProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
|
||||
{
|
||||
this.updatedFrontendStepList = updatedFrontendStepList;
|
||||
this.processMetaDataAdjustment = processMetaDataAdjustment;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for updatedFrontendStepList
|
||||
** Fluent setter for processMetaDataAdjustment
|
||||
*******************************************************************************/
|
||||
public ProcessState withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
public ProcessState withProcessMetaDataAdjustment(ProcessMetaDataAdjustment processMetaDataAdjustment)
|
||||
{
|
||||
this.updatedFrontendStepList = updatedFrontendStepList;
|
||||
this.processMetaDataAdjustment = processMetaDataAdjustment;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isStepBack
|
||||
*******************************************************************************/
|
||||
public boolean getIsStepBack()
|
||||
{
|
||||
return (this.isStepBack);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isStepBack
|
||||
*******************************************************************************/
|
||||
public void setIsStepBack(boolean isStepBack)
|
||||
{
|
||||
this.isStepBack = isStepBack;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isStepBack
|
||||
*******************************************************************************/
|
||||
public ProcessState withIsStepBack(boolean isStepBack)
|
||||
{
|
||||
this.isStepBack = isStepBack;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
private ArrayList<Serializable> primaryKeys;
|
||||
|
||||
private ArrayList<String> bulletsOfText;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -497,4 +498,35 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for bulletsOfText
|
||||
*******************************************************************************/
|
||||
public ArrayList<String> getBulletsOfText()
|
||||
{
|
||||
return (this.bulletsOfText);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for bulletsOfText
|
||||
*******************************************************************************/
|
||||
public void setBulletsOfText(ArrayList<String> bulletsOfText)
|
||||
{
|
||||
this.bulletsOfText = bulletsOfText;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for bulletsOfText
|
||||
*******************************************************************************/
|
||||
public ProcessSummaryLine withBulletsOfText(ArrayList<String> bulletsOfText)
|
||||
{
|
||||
this.bulletsOfText = bulletsOfText;
|
||||
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