Compare commits

..

318 Commits

Author SHA1 Message Date
a7faf52817 Merge branch 'release/0.11.0' 2023-01-30 13:37:55 -06:00
5a9a0754a6 Update versions for release 2023-01-30 13:31:17 -06:00
4790f55243 Fixing scheduled process context; better thread names; add serverInfo endpoint 2023-01-26 21:55:24 -06:00
f0450ef621 PRDONE-170 - Adding support for passing in custom ScriptUtls for scripts. 2023-01-26 12:45:07 -06:00
ed5839aa0a Add /run endpoint for running processes w/o any frontend 2023-01-26 11:23:41 -06:00
56f05c74fc Add secrets manager; update org.json 2023-01-26 11:15:47 -06:00
4f8f8bad9a Initial checkin 2023-01-26 11:15:02 -06:00
4a1853faa5 SPRINT-19: added more chart data and widget meta data 2023-01-25 19:13:35 -06:00
c972f9cecc Update to allow _qStepTimeoutMillis to come from formBody 2023-01-25 10:09:45 -06:00
efa796bb39 Add property/env to add processTag to all logs 2023-01-24 15:56:50 -06:00
a05e74a7d0 SPRINT-19: added stacked bar chart, more widget metadata 2023-01-24 15:52:15 -06:00
41fcb09c70 Update to reset memory record store before and after each 2023-01-24 14:22:09 -06:00
6b12390f6c make scheduler look at env var in addition to system property for deciding whether to start or not. 2023-01-24 14:19:55 -06:00
9889320fa6 add call to validate api responses to doInsert (POST) 2023-01-24 14:08:45 -06:00
c00120a1fc add overloads that take 'String message, LogPair... logPairs' 2023-01-24 13:52:44 -06:00
029f436071 Update to cache script and revision ids (could be better); new AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger, to just do 2 inserts across multiple script runs; add child-log-lines to script log table 2023-01-24 11:57:50 -06:00
f1ff53059f Handle null case in convertInputIdsToEnumIdType 2023-01-24 11:08:44 -06:00
18a8656ba2 Fix enum looks by ids of wrong type 2023-01-24 11:04:34 -06:00
e75b645639 Turning up logging 2023-01-20 15:14:10 -06:00
804efbd608 SPRINT-19: fixed xbar images 2023-01-20 13:13:29 -06:00
fe30d2654b update to implicity add a queryJoin, if a filter field calls out a table name that matches a join in the instance 2023-01-20 10:05:44 -06:00
97b803a86a add audit joins, move names to constants; fixes 2023-01-20 10:03:52 -06:00
2daa42aeb1 Merge branch 'feature/sprint-19' of github.com:Kingsrook/qqq into feature/sprint-19 2023-01-19 16:32:58 -06:00
4344fa472a Initial checkin 2023-01-19 16:31:18 -06:00
6bb810c1f4 Good exception for bad table name 2023-01-19 16:30:59 -06:00
d65af53090 Add some withers 2023-01-19 16:30:44 -06:00
ac6a7ba15a fix setValueIfTableHasField - should catch if field isn't found (helps tables w/o createDate work) 2023-01-19 16:12:30 -06:00
1c150e207a add nonNullArray and mergeLists 2023-01-19 16:12:02 -06:00
f9408716ac update to propagate default values into state before frontend steps 2023-01-19 16:11:45 -06:00
396f02265b explicit errors if context isn't setup 2023-01-19 16:11:19 -06:00
a1674792c6 Merge branch 'dev' into feature/sprint-19 2023-01-18 17:36:46 -06:00
22565b3ecd Initial checkin 2023-01-18 14:19:12 -06:00
d2e7b794f4 moving QLogger package 2023-01-18 12:11:40 -06:00
7000da409a moving standard lambdas to common package 2023-01-18 12:11:40 -06:00
3f0e09e32a Adding javalin access logs 2023-01-18 12:11:40 -06:00
70d9d259c1 Adding heavy field concept 2023-01-18 12:11:35 -06:00
4dc9c52ee0 Adding toLogPair method 2023-01-18 11:56:10 -06:00
964405d210 Initial checkin 2023-01-18 11:55:17 -06:00
369ba3c8d7 switch syslog to a json format (via patternLayout) 2023-01-18 11:46:12 -06:00
24d5406ee3 add filterStackTrace; move unsafeSupplier 2023-01-18 11:40:39 -06:00
984012f3a3 heavy fields, qvalueFormatter static 2023-01-18 11:39:47 -06:00
e8264d915f hotfix: updated to move pie chart label to be specified in the metacritic, added 'add' to pom resolver 2023-01-18 11:33:10 -06:00
46adfa8e24 WIP - logger migrations; initial work for data bag view widget 2023-01-17 10:44:45 -06:00
178078282c Switch to use QLogger everywhere 2023-01-17 10:44:45 -06:00
d03e947889 Merge branch 'dev' into feature/sprint-19 2023-01-17 10:29:16 -06:00
43de1cf749 Add post to download endpoint 2023-01-16 09:03:13 -06:00
d3fa1df56f Implementation of QContext everywhere, instead of passing QInstance and QSession in all ActionInputs 2023-01-15 19:41:23 -06:00
69a6104393 updated snapshot version, improved end of sprint script 2023-01-13 15:59:01 -06:00
e831c75e1e Merge branch 'dev' of github.com:Kingsrook/qqq into dev 2023-01-13 15:56:56 -06:00
eba9b0b4af Updating to <revision>0.11.0</revision> 2023-01-13 15:39:36 -06:00
9f82f35bcf Merge tag 'version-0.10.0' into dev
Tag release
2023-01-13 15:39:32 -06:00
ff07490f94 Merge branch 'release/0.10.0' 2023-01-13 15:38:34 -06:00
680696491f Update for next development version 2023-01-13 15:36:06 -06:00
a35e30059c Update versions for release 2023-01-13 15:36:05 -06:00
a37d22b0d0 Update export to work off post; add check for Authorization as a form param 2023-01-13 14:13:21 -06:00
9e02476ee7 Merge branch 'dev' 2023-01-13 11:02:59 -06:00
2b0974f4a5 Remove single-parent concept on app-children; more working version of recordLock from join 2023-01-13 09:42:31 -06:00
9a58c7683b SPRINT-18: updated module list to include slack and remove lambda 2023-01-12 21:07:55 -06:00
a556bf9764 SPRINT-18: added slack middleware module 2023-01-12 20:16:06 -06:00
41877a7055 Update to not setup a join context for non-table data sources 2023-01-12 08:38:09 -06:00
c07a77d4a6 Updated to disable view-all link if tablePath can't be found (e.g., because user can't view table) 2023-01-11 16:44:42 -06:00
23e9abeb74 implementation of record security locks, and permissions 2023-01-11 13:08:59 -06:00
e4d37e3db9 Add variant to pre-load by in-list, to cache misses as null 2023-01-05 14:33:32 -06:00
4b31a8b4bb Merge branch 'refs/heads/0.7.0-12-hotfix' into feature/sprint-18 2023-01-05 14:26:20 -06:00
d7e7315dc8 Merge branch 'feature/auth0-basic-auth-reuse' into feature/sprint-18 2023-01-05 11:36:39 -06:00
de05e4ae58 SPRINT-18: fixed unit test on auth0 reuse 2023-01-04 22:20:38 -06:00
150582964b Change setupSession to be public 2023-01-04 12:05:19 -06:00
2874b98b66 Add re-use of tokens from basicAuth 2023-01-04 10:07:44 -06:00
7fae3e2329 Add table-based authentication module; update javalin to support Authentication: Basic header; Move authentication classes 2022-12-28 17:00:08 -06:00
428f48602b Better testing on join reports, possible value translations; renamed left & right in QueryJoin (now joinTable, baseTable) 2022-12-22 13:46:32 -06:00
799b695e14 Checkpoint on report and export changes, possible value translating 2022-12-21 11:37:16 -06:00
19d88910b5 Updating table sync api 2022-12-20 10:56:59 -06:00
2ad4b22f55 Add withSchedule method 2022-12-19 15:02:20 -06:00
040dae55d5 Add withSchedule; fix return on withBasepullConfiguration; add overload withTransformStepClass 2022-12-19 14:59:58 -06:00
dd9253fde4 Updated comment about field behaviorfs 2022-12-19 14:56:37 -06:00
8a4d5bfb34 Update to not NPE if data source doesn't have a filter 2022-12-19 14:56:25 -06:00
e1c53b9d48 Updated interface in sync processes; more status updates in ETL processes; Basepull only update timestamp if ran as basepull; javalin report endpoint; 2022-12-19 12:04:01 -06:00
a6656af040 Updated for older api on QQueryFilter (noarg constructor) 2022-12-15 16:18:14 -06:00
0e68bf1e72 Hotfix - sqs batch mode, and rdbms delete-by-filter 2022-12-15 16:13:56 -06:00
1b672afcd0 Fixed rdbms delete test 2022-12-15 16:04:46 -06:00
5005c38c18 Fixes for performance of sqs (batch mode), plus bug in deleteAction by query-filter (with test that proves it) 2022-12-15 15:54:58 -06:00
1cdf437551 SPRINT-18: upped current snapshot version 2022-12-15 15:39:33 -06:00
04cddb8d5d Updating to <revision>0.9.0</revision> 2022-12-15 15:34:43 -06:00
0d6bd013f5 Merge branch 'release/0.9.0' 2022-12-15 15:33:49 -06:00
7b6284c06f Update for next development version 2022-12-15 15:24:00 -06:00
edcc27485e Update versions for release 2022-12-15 15:23:59 -06:00
280373ddc5 SPRINT-17: added code coverage test class to meet minimum requirements 2022-12-15 10:36:43 -06:00
25815ebc25 SPRINT-17: updated some widgets to look less broken when data is 'not available now', checkpoint commit on 'real dashboards' 2022-12-15 10:20:48 -06:00
59cbf83860 Add field increaseIsGood 2022-12-14 14:53:03 -06:00
6186b17e92 Add environmentValues to qInstance 2022-12-14 14:50:45 -06:00
30003b729c Add overload of getForeignRecordMap that takes additional (base) filter for query 2022-12-09 20:25:59 -06:00
293b3e4207 Add qruntime exception; let transform step set pipe capacity 2022-12-09 16:44:53 -06:00
470321dcf6 add equals and hashcode 2022-12-09 16:06:26 -06:00
48cfdeffa1 Change post-query customizer to be class (that can do list), not function 2022-12-09 12:05:39 -06:00
14c7fbe370 Moving dropdowns to work for all widgets 2022-12-09 09:53:13 -06:00
8454f94020 SPRINT-17: updated dropdowns to be required, added divider 2022-12-08 15:06:40 -06:00
697261f91b Add aHrefViewRecord 2022-12-08 10:37:11 -06:00
61d493a4f5 Add FieldValueList widget type, more html-helper methods 2022-12-08 09:18:52 -06:00
c21c89e85f SPRINT-17: test was not using groupby to fetch values 2022-12-07 16:43:27 -06:00
a9a3e3b19e SPRINT-17: fixed dumb checkstyle violation 2022-12-07 16:31:04 -06:00
5cf9e7b60d SPRINT-17: fixed failing test and removed pvs name member from parent meta data 2022-12-07 15:47:15 -06:00
9b34ee7fe7 SPRINT-17: updates to parent widget dropdown data, updated group bys to be objects allowing group by with custom formats 2022-12-07 15:31:48 -06:00
241741e2e5 Fix default for canAddChildRecord to be false; add more link-generating functions (to child-modals on view screens, etc) 2022-12-06 15:57:32 -06:00
a769d8942c Adding unique key check to insert action; adding post-insert customizer 2022-12-05 15:46:17 -06:00
c22fc89cbb SPRINT-17: changed some variable names 2022-12-05 12:18:23 -06:00
060da69afb Adding table-cacheOf concept; ability to add a child record from child-list widget 2022-12-05 10:26:53 -06:00
3691ad87e5 Merge remote-tracking branch 'origin/0.7.0-12-hotfix' into dev 2022-12-01 16:59:19 -06:00
583ac70563 Merge tag 'version-0.8.0' into dev
Tag release
2022-12-01 16:56:13 -06:00
2afd8864a8 Merge branch 'release/0.8.0' 2022-12-01 16:53:46 -06:00
28e0bdadbf Update for next development version 2022-12-01 16:49:34 -06:00
c856a7b0b3 updated versions for release 2022-12-01 16:33:37 -06:00
3a721d8df5 cd to correct directory after environment setup 2022-12-01 15:56:52 -06:00
ab08a99477 updated to move to proper directory before beginning work 2022-12-01 15:44:34 -06:00
e170dab1db SPRINT-16: added ERROR adornment type 2022-11-30 16:52:20 -06:00
40afb629e7 Add overrideBatchSize to table automation details 2022-11-30 11:30:18 -06:00
ce2cccfa2a SPRINT-16: added more tests 2022-11-30 11:29:48 -06:00
6813617a21 SPRINT-16: added new widget types, moved some things to a different package, etc. 2022-11-29 14:34:59 -06:00
2df9576e20 Initial checkin 2022-11-28 15:19:14 -06:00
792383d21a Add ConvertHtmlToPdfAction 2022-11-28 11:52:48 -06:00
b2d76e8206 Much implementation of joins for RDBMS 2022-11-23 16:37:54 -06:00
6685e61500 Merge branch 'dev' into feature/sprint-16 2022-11-22 13:01:50 -06:00
aef8f5cd59 Adding maxLength to fields, along with initial version of FieldBehviors and ValueBehaviorApplier, including ValueTooLongBehavior 2022-11-22 12:59:57 -06:00
26dd323f34 SPRINT-16: upped current snapshot version 2022-11-22 12:24:06 -06:00
209ada8065 Add more util methods; add AbstractHTMLWidgetRenderer 2022-11-21 14:46:14 -06:00
105b2c92c9 Add aggregateAction; Add renderTemplateAction 2022-11-18 16:51:54 -06:00
1d1461deea Adding filesystem writing - used by javalin to store uploaded files; done async, via new base class for actions 2022-11-17 19:59:29 -06:00
6b2860e303 Initial checkin 2022-11-17 15:59:13 -06:00
0233edc7b2 Updating to <revision>0.7.0</revision> 2022-11-17 12:13:02 -06:00
f2dbe5bf12 Merge branch 'release/0.7.0' 2022-11-17 12:12:12 -06:00
31c1a8ecb6 Update for next development version 2022-11-17 12:07:27 -06:00
373ff20e9d Update versions for release 2022-11-17 12:07:26 -06:00
3305fb76d5 Fixed to insert & update correct lists 2022-11-16 14:42:22 -06:00
d2d32e5ab1 SPRINT-15: reverted log change 2022-11-16 10:22:16 -06:00
2920774266 SPRINT-15: reverted log change 2022-11-16 10:18:57 -06:00
03bce59f2a SPRINT-15: reverted log change 2022-11-16 10:14:14 -06:00
955a30a660 SPRINT-15: attempt to improve json logging 2022-11-16 08:55:02 -06:00
d2bfe9f610 SPRINT-15: attempt to improve json logging 2022-11-15 21:44:28 -06:00
798dc6e6e5 Merge branch 'feature/sprint-15' of github.com:Kingsrook/qqq into feature/sprint-15 2022-11-15 21:15:23 -06:00
126d51c8f9 SPRINT-15: attempt to improve json logging 2022-11-15 21:14:06 -06:00
7d72487a75 Initial checkin 2022-11-15 15:41:46 -06:00
848a09cef5 SPRINT-15: attempt to fix loggly datetimes 2022-11-15 12:17:32 -06:00
322efa2102 Initial checkin 2022-11-15 10:47:49 -06:00
20607bbf9b SPRINT-15: added initial version of the QLogger class 2022-11-14 17:37:47 -06:00
430e1bc9c7 Add table name to record pipe loop 2022-11-14 14:00:55 -06:00
2a7e76b0f9 Add joins and ChildRecordList widget 2022-11-14 14:00:55 -06:00
f0a464ce9e SPRINT-15: trying to fix circle ci 2022-11-14 12:30:02 -06:00
e305c64572 SPRINT-15: trying to fix circle ci 2022-11-14 12:26:56 -06:00
514f105c86 SPRINT-15: ¯\_(ツ)_/¯ 2022-11-14 12:06:57 -06:00
87dc7fd96c SPRINT-15: adding reinstalling of ca-certificates to try to fix circle ci failures 2022-11-14 11:38:20 -06:00
d54e0c71a8 SPRINT-15: moved response validation into its own method which can be overriden in subclasses 2022-11-14 10:47:02 -06:00
a4ad8ac08a SPRINT-15: added default constructor 2022-11-13 14:35:46 -06:00
b2aaffeb6d SPRINT-15: updates to logging 2022-11-11 21:48:50 -06:00
11aa55cf40 SPRINT-15: refactor of api actions, moving logics into utils class, unified all calls to apis, clean ups, etc. 2022-11-11 21:39:05 -06:00
a2da8c4127 Add withValues(Pair...) and iconAndColorValues 2022-11-11 16:20:50 -06:00
8b31cee890 Add TableSyncProcess 2022-11-11 14:34:13 -06:00
a5ec33b51b Script Tests and further enhancements 2022-11-10 14:22:43 -06:00
6d08afa4c2 SPRINT-15: added json util helper method 2022-11-10 13:21:11 -06:00
6496986aab bump 2022-11-09 11:05:08 -06:00
f9c14eb08c update to use same try-with-resources for CloseableHttpClient and CloseableHttpResponse 2022-11-09 11:02:25 -06:00
230dde2e52 Refactoring javascript executor scripts.main error handling 2022-11-09 10:36:37 -06:00
e701ae0ea3 add try-catch around script.main business 2022-11-09 10:03:14 -06:00
955294ae18 Update javascript executor to work w/ compiled ts scripts that export a main function; add output to javalin storeRecordAssociatedScript 2022-11-09 09:49:58 -06:00
2975b90505 Allowing load step to set pipe sizes to avoid 'Giving up adding record to pipe' in easypost tracker creation; Make status never have current > total; 2022-11-09 08:44:16 -06:00
1e09931218 SPRINT-15: updated apiupdateAction to handle 200/207 better, added better exception handling 2022-11-08 16:33:06 -06:00
8932ef891e ADd method objectToMap 2022-11-08 09:28:37 -06:00
1a287fe35a Log and thread name adjustments - trying to make loggly more useful 2022-11-08 09:08:15 -06:00
236eff523e SPRINT-15: added methods to get lists and maps of record entities 2022-11-04 14:28:15 -05:00
e815bd37ab Merge branch 'dev' into feature/sprint-15 2022-11-04 10:02:33 -05:00
669b6d3cb7 Adding status object in standard loadVia steps and updating it in api insert; add user timezone header to session 2022-11-04 09:59:07 -05:00
f99430d2bc sprint-15:updated current snapshot version 2022-11-03 15:09:28 -05:00
ebf9556a5d Merge branch 'feature/sprint-14' into feature/sprint-15 2022-11-03 14:36:32 -05:00
a3f9df09a9 Updating to 0.7.0 2022-11-03 14:31:42 -05:00
2105e3dfc5 sprint-14: changed update to do a post not put 2022-11-03 14:29:40 -05:00
99561ecf81 Merge tag 'version-0.6.0' into dev
Tag release
2022-11-03 11:57:15 -05:00
30ecd2d331 Merge branch 'release/0.6.0' 2022-11-03 11:56:25 -05:00
bd85526261 Update for next development version 2022-11-03 11:45:59 -05:00
11648a5759 Update versions for release 2022-11-03 11:45:58 -05:00
e433b78e5b Merge pull request #10 from Kingsrook/feature/sprint-14
Feature/sprint 14
2022-11-03 11:43:31 -05:00
ce9316453b PRDONE-136 - Adding support for basic auth login via auth0 2022-11-03 11:36:18 -05:00
ab0b38dd82 Add RunReportForRecordProcess; 1st version of AbstractProcessMetaDataBuilder 2022-11-03 10:29:44 -05:00
3c04841f73 sprint-14: initial checkin of api update action 2022-11-02 11:04:05 -05:00
683b3c658d Cleanup from code review 2022-11-01 16:21:30 -05:00
165583cd98 Initial version of scripts, javascript 2022-10-31 15:48:27 -05:00
0ada444fd4 sprint-14: moved 'final' before 'static' 2022-10-28 11:12:57 -05:00
662fefea19 sprint-14: put json data into backend details 2022-10-28 11:09:35 -05:00
1cdb4b37e9 sprint-14: fixed test which expected an instant but now receives a string 2022-10-27 13:22:14 -05:00
622183e276 sprint-14: moved getting of timestamps into their own methods that can be overridden by subclasses 2022-10-27 13:17:38 -05:00
80af7a3710 Add RunProcessAction.BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD 2022-10-27 11:17:08 -05:00
e6db303bc2 Fix last build 2022-10-27 11:12:27 -05:00
da116349ff Update to only update basepull timestamp after execution; make sure preRun runs before count 2022-10-27 11:02:47 -05:00
5e7b6f40df Add path to icon; other cleanup 2022-10-26 18:09:07 -05:00
82810c2b66 Add ExtractViaBasepullQueryStep; add pagination & piping to api query 2022-10-26 12:31:09 -05:00
77927fd318 sprint-14: minor updates to allow flexibility when extending loadviainsertorupdatestep 2022-10-26 11:56:17 -05:00
033dbeb76c Update to run an executeStep pre-action 2022-10-25 13:31:48 -05:00
8ffc1c1a63 udpated api json parsing (lenient mode); add escaping table names in rdbms 2022-10-25 10:47:06 -05:00
dae803f709 Initial checkin of QueryStringBuilder 2022-10-25 09:55:28 -05:00
128c379f10 sprint-14: initial checkin of basepull capability on processes 2022-10-24 17:06:11 -05:00
44537e182d Add dedicated method for api count in baseApiActionUtil; improve null handling in json to record 2022-10-24 15:34:37 -05:00
22b2e01cca Add withSectionOfFields 2022-10-24 08:40:04 -05:00
f5f6446069 Add GET action, and usage in API 2022-10-21 14:40:38 -05:00
204d67dd21 Merge branch 'main' into dev
# Conflicts:
#	qqq-backend-core/pom.xml
2022-10-21 13:42:30 -05:00
234ec4823b Merge pull request #8 from Kingsrook/feature/sprint-11
Feature/sprint 11
2022-10-21 12:21:40 -05:00
fa2ab18e30 Add unique keys, and checking of them in bulk load; add some more validation (sqs and unique keys) 2022-10-21 11:23:30 -05:00
20c42deae5 Fix where automation status got set to OK instead of running; switch to do an automation polling thread per-table/status 2022-10-20 10:48:58 -05:00
8b3b300eb1 Add custom values to api backend meta data 2022-10-19 18:02:35 -05:00
18a3f72c4a updated api backend to support count and query 2022-10-19 10:43:39 -05:00
bf3835bd4c Set to 0 covered ratio 2022-10-19 10:35:19 -05:00
84ccd92a6e Add rate-limit records to output 2022-10-19 10:33:01 -05:00
0c37630754 Disablign in CI 2022-10-19 10:32:51 -05:00
98e846d1f1 add try-catch around value casting 2022-10-19 09:27:41 -05:00
b12de62295 Fix bulk process PVS rendering 2022-10-19 09:09:44 -05:00
bb975e7c6a Merge pull request #9 from Kingsrook/dependabot/maven/qqq-backend-core/com.fasterxml.jackson.core-jackson-databind-2.13.4.1
Bump jackson-databind from 2.13.2.1 to 2.13.4.1 in /qqq-backend-core
2022-10-19 08:40:31 -05:00
8423a8db2e Bump jackson-databind from 2.13.2.1 to 2.13.4.1 in /qqq-backend-core
Bumps [jackson-databind](https://github.com/FasterXML/jackson) from 2.13.2.1 to 2.13.4.1.
- [Release notes](https://github.com/FasterXML/jackson/releases)
- [Commits](https://github.com/FasterXML/jackson/commits)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-19 06:42:19 +00:00
2d3d1091fd Add basic rate limit handling for POST 2022-10-18 11:12:25 -05:00
6345846eba Adding queues and queue providers; Adding schedules and ScheduleManager 2022-10-18 09:00:21 -05:00
888239b265 Refactored lambda to do standard qqq actions (once implemented) 2022-10-14 16:55:59 -05:00
e3cc2e63f6 Initial checkin of lambda module 2022-10-14 11:45:15 -05:00
dbfc2fe2d8 Initial checkin of lambda module 2022-10-14 11:07:43 -05:00
37fbfd1c7c Update to accept count filter as POST 2022-10-14 10:19:21 -05:00
117bb621ff Update to accept query & filter as POST 2022-10-14 10:11:53 -05:00
7c339b4e81 Add qqq-backend-module-api 2022-10-12 18:24:17 -05:00
471954e8b9 Initial checkin of API module 2022-10-12 18:00:08 -05:00
b91273a53a Add LoadViaInsertOrUpdateStep; make PVS field labels not have Id suffix; add populateFromQRecord 2022-10-11 16:37:33 -05:00
aa64f1b7f3 Update to only call consumer after the loop when it's smart to do so 2022-10-11 11:30:28 -05:00
6ec6b173b9 Updated to not try to prime PVS cache for non-table PVS's 2022-10-11 08:41:45 -05:00
c43a8998ec Updating to support possible value searching 2022-10-10 17:01:15 -05:00
262038bc87 Adding booleanOperator and subFilters to QQueryFilter 2022-10-10 09:13:16 -05:00
28060e95e3 Fixed NPE with null value from possible values 2022-10-07 12:24:27 -05:00
d0c26719e1 SPRINT-12: added test for Qbackend module interface 2022-10-07 10:19:23 -05:00
1548d7f35b Initial checkin 2022-10-07 10:14:35 -05:00
e53d559b29 SPRINT-12: added process step class validation, added noop transform step 2022-10-06 19:44:27 -05:00
73df50add1 Handle booleans better 2022-10-04 11:33:04 -05:00
c5a3534d43 Fix query bugs w/ in-list, empty; format booleans as Yes/No 2022-10-04 09:56:33 -05:00
e3903c0ab9 SPRINT-12: fixed broken test due to types on widgets now 2022-10-03 14:15:46 -05:00
ba4fddb7e5 SPRINT-12: fixed broken test due to types on widgets now 2022-10-03 14:07:10 -05:00
bb85d362fb SPRINT-12: added widget meta data 2022-10-03 14:02:55 -05:00
7415732c6c Update to translate possible values when building other possible values... 2022-10-03 13:31:13 -05:00
21cd07b2df Update scrubValues method to make instants out of DateTimes - fixes update actions in javalin apps 2022-10-03 10:40:45 -05:00
3f84271a36 Feedback from code reviews 2022-10-03 09:09:06 -05:00
17cace070c Change process summary line to interface; add record-link summary-line 2022-09-30 13:32:18 -05:00
456364de2a Add static data provider capability to reports 2022-09-29 16:55:08 -05:00
2d2eae5c06 Add rollback of transaction in last test 2022-09-29 14:39:43 -05:00
86ebe6ee4e Add transaction to transform step and query action (and rdbms query) 2022-09-29 14:34:51 -05:00
8a1110abf0 SPRINT-12: added stepper widget, added linkability to table widget 2022-09-29 12:04:24 -05:00
720200b6cf Update to clone query filters 2022-09-28 14:49:46 -05:00
70ded4c887 Add conversion of instant to localDate 2022-09-28 08:22:51 -05:00
89c9c72772 make all the method names not be the same 2022-09-27 19:05:10 -05:00
4d16bc0fc7 Auto-adorn PVS tables with link-to-record; change query to fetch instants, not LocalDateTimes 2022-09-27 18:59:58 -05:00
3587cc0676 Updated the update action, to set a null value for fields that came in the request as empty string 2022-09-27 14:14:46 -05:00
833b1f9643 SPRINT-12: added 'multi statitistics' widget 2022-09-27 14:14:36 -05:00
c002522cb8 Update to use column labels, when specified, in reports 2022-09-26 11:27:50 -05:00
3402a20b04 SPRINT-12: added rawHTML widget type 2022-09-26 10:18:25 -05:00
ac82928dd7 Do possible-values before display values, so a rendered possible-value can be part of a record label 2022-09-26 08:37:05 -05:00
d73e546c7b Add field adornments (links, chips, widths) 2022-09-23 17:01:46 -05:00
3ac6b34963 SPRINT-12: more tests fixed 2022-09-23 17:00:52 -05:00
742cf7fa3b SPRINT-12: fixed compile error due to merging 2022-09-23 16:26:13 -05:00
ce48933cbd SPRINT-12: fixed style issue and broken tests 2022-09-23 16:21:38 -05:00
5efd2da636 SPRINT-12: updated to define widgets at table level, refactoring of some of the widget stuff to match other "Action"s 2022-09-23 15:55:27 -05:00
3d07d215a9 Adding format date & time methods 2022-09-23 14:17:08 -05:00
9397934769 QQQ-42 adding data-sources to reports, customizer points 2022-09-23 09:55:34 -05:00
f83d2b3fc8 Merge branch 'QQQ-41-v-2-of-app-home-pages-dashboards-etc' into feature/sprint-11 2022-09-20 15:20:05 -05:00
d8c6bba6b4 QQQ-41: checkpoint commit for moving to next sprint 2022-09-20 15:18:49 -05:00
d5a319c458 Merge branch 'feature/QQQ-42-reports' into feature/sprint-11 2022-09-20 12:49:37 -05:00
1f546d8c7d QQQ-42 checkpoint of qqq reports 2022-09-20 12:46:32 -05:00
197dec3105 Merge remote-tracking branch 'origin/QQQ-41-v-2-of-app-home-pages-dashboards-etc' into feature/QQQ-42-reports 2022-09-19 13:52:54 -05:00
525389e62e QQQ-42 checkpoint of qqq reports 2022-09-19 13:52:43 -05:00
80ff7a26e0 QQQ-41: fixed failing test 2022-09-16 11:01:26 -05:00
0b10717116 Merge branch 'QQQ-41-v-2-of-app-home-pages-dashboards-etc' into feature/sprint-11 2022-09-16 10:51:24 -05:00
8891b3e7ea QQQ-41: added app sections, new widgets, ability to have no tables under app, etc. 2022-09-16 10:19:06 -05:00
01afdaacfd More live templates 2022-09-14 14:03:29 -05:00
112c62d035 Initial checkin 2022-09-14 13:59:34 -05:00
1e1c07958b Merge branch 'feature/QQQ-42-reports' into feature/sprint-11 2022-09-14 13:12:13 -05:00
b05c5749b4 QQQ-42 initial implementation of qqq reports (pivots, WIP) 2022-09-14 13:05:31 -05:00
247e419e07 Updated for new assumptions re: fieldSection labels 2022-09-09 15:19:57 -05:00
2f787036d0 Update to set QFieldSection label from name; try to avoid backend-module-not-defined errors 2022-09-09 15:13:28 -05:00
a1f5e90106 Initial import from qqq-dev-tools standalone repo 2022-09-09 15:00:20 -05:00
bee8a7a2d9 Merge tag 'version-0.5.0' into dev
Tag release
2022-09-08 11:18:10 -05:00
d34555cfb2 Merge branch 'release/0.5.0' 2022-09-08 11:17:25 -05:00
9a95fe36e6 Update for next development version 2022-09-08 11:12:56 -05:00
79607a8cac Update versions for release 2022-09-08 11:12:55 -05:00
278910e4ee Merge pull request #7 from Kingsrook/feature/sprint-10
Feature/sprint 10
2022-09-08 11:09:43 -05:00
0d0a7aec1c Initial checkin 2022-09-08 10:58:21 -05:00
b01758879c QQQ-37 Redo bulk processes in streamed-etl mode 2022-09-07 16:59:42 -05:00
1c75df3a09 added initial version of branding as metadata 2022-09-06 19:00:12 -05:00
25c9376ce4 QQQ-37 update streamed-etl steps to not have to use different record-list 2022-09-06 15:15:11 -05:00
31e6bf4d49 PRDONE-94: updates from code review feedback added .env test 2022-09-06 11:41:25 -05:00
12925127b2 Feedback from code reviews 2022-09-06 09:29:24 -05:00
9a8b49f1a7 Feedback from code reviews 2022-09-05 09:47:43 -05:00
4af7757fdd Merge commit '3a69ce7' into feature/sprint-10 2022-09-01 15:59:50 -05:00
91ba6f4b4e Merge branch 'feature/sprint-10' of github.com:Kingsrook/qqq into feature/sprint-10 2022-09-01 15:59:19 -05:00
3a69ce7d2f QQQ-40 getting closer to production-ready on automations 2022-09-01 15:57:01 -05:00
64e801747f QQQ-40 getting closer to production-ready on automations 2022-09-01 15:53:35 -05:00
6b4417d3e8 Merge branch 'feature/QQQ-38-app-home-widgets' into feature/sprint-10 2022-08-31 15:59:32 -05:00
e157809b35 Merge branch 'feature/QQQ-40-record-automations' into feature/sprint-10 2022-08-31 15:12:54 -05:00
a08ec0ae6f QQQ-40 Initial working POC 2022-08-31 15:01:45 -05:00
f08ffe691f PRDONE-94: updated to use interpreter for getting environment credentials, updated interpreter to load Dotenv files as environment overrides 2022-08-31 12:05:35 -05:00
69b9ed5b19 QQQ-37 adding pre & post run to ETL transform & load; minor QOL 2022-08-30 13:44:34 -05:00
4bf1fe8638 PRDONE-94: updated to look for dotenv properties and fall back to environment vars 2022-08-30 13:33:13 -05:00
dcea96579c PRDONE-94: updated to set dotenv variables as system properties 2022-08-30 12:28:40 -05:00
4c2ebf8a94 PRDONE-94: updated to ignore missing .env file 2022-08-30 12:17:37 -05:00
4316b47916 fixed incorrect import order 2022-08-30 12:13:29 -05:00
48b8d295e3 initial checkin of quicksight dashboard widget POC, updated to remove hard coded credentials 2022-08-30 11:46:46 -05:00
6142b8e703 Update tests to run w/ h2 instead of mysql 2022-08-29 14:29:01 -05:00
bc95899a50 Increase coverage 2022-08-29 14:20:00 -05:00
9106b82560 Fixing tests 2022-08-29 14:12:09 -05:00
39f065e23e Fixing tests 2022-08-29 13:52:54 -05:00
cb22f86793 Checkpoint - working versions of streamed with frontend processes, with validation 2022-08-29 13:33:35 -05:00
d32538bf45 Merge branch 'dev' into feature/QQQ-38-app-home-widgets 2022-08-25 10:14:26 -05:00
5f8f063b99 Merge branch 'dev' into feature/QQQ-37-streamed-processes 2022-08-25 10:09:23 -05:00
c3bba7cf8c Merge tag 'version-0.4.0' into dev
Tag release
2022-08-25 10:08:05 -05:00
103d229c82 Update for next development version 2022-08-25 10:01:52 -05:00
7355a9c8aa QQQ-37 Updating test coverage 2022-08-24 10:18:59 -05:00
c2972cd4df QQQ-37 checkpoint 2022-08-23 16:54:36 -05:00
834b4136de Merge branch 'feature/sprint-9-support-updates' into feature/QQQ-37-streamed-processes 2022-08-23 11:17:45 -05:00
459a533f60 upated to work in CI (e.g., w/o database) 2022-08-22 10:53:51 -05:00
937304e7f1 QQQ-38 Initial build of app home page widgets 2022-08-22 10:48:55 -05:00
21d66cc7fc QQQ-37 added test coverage for StreamedETLWithFrontendProcess 2022-08-22 10:08:27 -05:00
a86f42f373 QQQ-37 initial buidout of StreamedETLWithFrontendProcess 2022-08-22 08:36:09 -05:00
640 changed files with 82035 additions and 4050 deletions

View File

@ -20,7 +20,7 @@ commands:
- run:
name: Install Java 17
command: |
sudo add-apt-repository -y ppa:openjdk-r/ppa
sudo apt-get update
sudo apt install -y openjdk-17-jdk
sudo rm /etc/alternatives/java
sudo ln -s /usr/lib/jvm/java-17-openjdk-amd64/bin/java /etc/alternatives/java

3
.gitignore vendored
View File

@ -2,6 +2,7 @@ target/
*.iml
.env
.idea
#############################################
## Original contents from github template: ##
@ -29,6 +30,8 @@ target/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
.DS_Store
*.swp
.flattened-pom.xml
dependency-reduced-pom.xml

View File

@ -179,9 +179,7 @@
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="1"/>
</module>
-->
<module name="OverloadMethodsDeclarationOrder"/>
<!--
<module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
@ -261,5 +259,10 @@
<module name="MissingOverride"/>
</module>
<module name="Header">
<property name="headerFile" value="checkstyle/license.txt"/>
<property name="fileExtensions" value="java"/>
<property name="ignoreLines" value="3"/>
</module>
<module name="SuppressWarningsFilter"/>
</module>

20
checkstyle/license.txt Normal file
View File

@ -0,0 +1,20 @@
/*
* 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/>.
*/

30
pom.xml
View File

@ -30,15 +30,20 @@
<modules>
<module>qqq-backend-core</module>
<module>qqq-backend-module-rdbms</module>
<module>qqq-backend-module-api</module>
<module>qqq-backend-module-filesystem</module>
<module>qqq-backend-module-rdbms</module>
<module>qqq-language-support-javascript</module>
<module>qqq-middleware-picocli</module>
<module>qqq-middleware-javalin</module>
<module>qqq-middleware-lambda</module>
<module>qqq-utility-lambdas</module>
<module>qqq-sample-project</module>
<module>qqq-middleware-slack</module>
</modules>
<properties>
<revision>0.4.0</revision>
<revision>0.11.0</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@ -49,8 +54,20 @@
<coverage.haltOnFailure>true</coverage.haltOnFailure>
<coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.95</coverage.classCoveredRatioMinimum>
<plugin.shade.phase>none</plugin.shade.phase>
</properties>
<profiles>
<profile>
<!-- For qqq-middleware-lambda - to build its shaded jar, its qqq dependencies also need
to build a shaded jar. So to activate that mode, use this profile (-P buildShadedJar)-->
<id>buildShadedJar</id>
<properties>
<plugin.shade.phase>package</plugin.shade.phase>
</properties>
</profile>
</profiles>
<dependencyManagement>
<dependencies>
<dependency>
@ -74,6 +91,12 @@
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
@ -122,7 +145,8 @@
<id>validate</id>
<phase>validate</phase>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<configLocation>checkstyle/config.xml</configLocation>
<headerLocation>checkstyle/license.txt</headerLocation>
<!-- <suppressionsLocation>checkstyle-suppressions.xml</suppressionsLocation> -->
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput>

View File

@ -36,15 +36,36 @@
<!-- noe at this time -->
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.17.259</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- other qqq modules deps -->
<!-- none, this is core. -->
<!-- 3rd party deps specifically for this module -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>quicksight</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-secretsmanager</artifactId>
<version>1.12.385</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.2.1</version>
<version>2.14.0-rc1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
@ -54,7 +75,7 @@
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
<version>20220320</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
@ -82,6 +103,46 @@
<artifactId>java-dotenv</artifactId>
<version>5.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<!-- the next 2 deps are for html to pdf - per https://www.baeldung.com/java-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>
</dependency>
<!-- the next 3 deps are being added for google drive support -->
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>1.35.2</version>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-drive</artifactId>
<version>v3-rev20220815-2.0.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId>
<version>1.12.321</version>
</dependency>
<!-- Common deps for all qqq modules -->
<dependency>
@ -101,12 +162,24 @@
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@ -121,6 +194,30 @@
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>${plugin.shade.phase}</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -0,0 +1,77 @@
/*
* 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;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
/*******************************************************************************
** Base class for QQQ Actions (both framework and application defined) that
** have a signature like a BiConsumer - taking both Input and Output objects as
** parameters, with void output.
*******************************************************************************/
public abstract class AbstractQActionBiConsumer<I extends AbstractActionInput, O extends AbstractActionOutput>
{
/*******************************************************************************
**
*******************************************************************************/
public abstract void execute(I input, O output) throws QException;
/*******************************************************************************
**
*******************************************************************************/
public Future<Void> executeAsync(I input, O output)
{
CapturedContext capturedContext = QContext.capture();
CompletableFuture<Void> completableFuture = new CompletableFuture<>();
Executors.newCachedThreadPool().submit(() ->
{
try
{
QContext.init(capturedContext);
execute(input, output);
completableFuture.complete(null);
}
catch(QException e)
{
completableFuture.completeExceptionally(e);
}
finally
{
QContext.clear();
}
});
return (completableFuture);
}
}

View File

@ -0,0 +1,77 @@
/*
* 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;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
/*******************************************************************************
** Base class for QQQ Actions (both framework and application defined) that
** have a signature like a Function - taking an Input object as a parameter,
** and returning an Output object.
*******************************************************************************/
public abstract class AbstractQActionFunction<I extends AbstractActionInput, O extends AbstractActionOutput>
{
/*******************************************************************************
**
*******************************************************************************/
public abstract O execute(I input) throws QException;
/*******************************************************************************
**
*******************************************************************************/
public Future<O> executeAsync(I input)
{
CapturedContext capturedContext = QContext.capture();
CompletableFuture<O> completableFuture = new CompletableFuture<>();
Executors.newCachedThreadPool().submit(() ->
{
try
{
QContext.init(capturedContext);
O output = execute(input);
completableFuture.complete(output);
}
catch(QException e)
{
completableFuture.completeExceptionally(e);
}
finally
{
QContext.clear();
}
});
return (completableFuture);
}
}

View File

@ -22,9 +22,15 @@
package com.kingsrook.qqq.backend.core.actions;
import java.io.Serializable;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
@ -40,12 +46,37 @@ public class ActionHelper
*******************************************************************************/
public static void validateSession(AbstractActionInput request) throws QException
{
QInstance qInstance = QContext.getQInstance();
QSession qSession = QContext.getQSession();
if(qInstance == null)
{
throw (new QException("QInstance was not set in QContext."));
}
if(qSession == null)
{
throw (new QException("QSession was not set in QContext."));
}
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData());
if(!authenticationModule.isSessionValid(request.getInstance(), request.getSession()))
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
if(!authenticationModule.isSessionValid(qInstance, qSession))
{
throw new QAuthenticationException("Invalid session in request");
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void editFirstValue(List<Serializable> values, Function<String, String> editFunction)
{
if(values.size() > 0)
{
values.set(0, editFunction.apply(String.valueOf(values.get(0))));
}
}
}

View File

@ -31,7 +31,6 @@ import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
** Argument passed to an AsyncJob when it runs, which can be used to communicate
** data back out of the job.
**
** TODO - future - allow cancellation to be indicated here?
*******************************************************************************/
public class AsyncJobCallback
{
@ -51,6 +50,17 @@ public class AsyncJobCallback
/*******************************************************************************
** Setter for asyncJobStatus
**
*******************************************************************************/
public void setAsyncJobStatus(AsyncJobStatus asyncJobStatus)
{
this.asyncJobStatus = asyncJobStatus;
}
/*******************************************************************************
** Update the message
*******************************************************************************/
@ -68,9 +78,7 @@ public class AsyncJobCallback
public void updateStatus(String message, int current, int total)
{
this.asyncJobStatus.setMessage(message);
this.asyncJobStatus.setCurrent(current);
this.asyncJobStatus.setTotal(total);
storeUpdatedStatus();
updateStatus(current, total); // this call will storeUpdatedStatus.
}
@ -80,13 +88,48 @@ public class AsyncJobCallback
*******************************************************************************/
public void updateStatus(int current, int total)
{
this.asyncJobStatus.setCurrent(current);
this.asyncJobStatus.setCurrent(current > total ? total : current);
this.asyncJobStatus.setTotal(total);
storeUpdatedStatus();
}
/*******************************************************************************
** Increase the 'current' value in the '1 of 2' sense.
*******************************************************************************/
public void incrementCurrent()
{
incrementCurrent(1);
}
/*******************************************************************************
** Increase the 'current' value in the '1 of 2' sense.
*******************************************************************************/
public void incrementCurrent(int amount)
{
if(asyncJobStatus.getCurrent() != null)
{
if(asyncJobStatus.getTotal() != null && asyncJobStatus.getCurrent() + amount > asyncJobStatus.getTotal())
{
/////////////////////////////////////////////////////
// make sure we don't ever make current > total... //
/////////////////////////////////////////////////////
asyncJobStatus.setCurrent(asyncJobStatus.getTotal());
}
else
{
asyncJobStatus.setCurrent(asyncJobStatus.getCurrent() + amount);
}
storeUpdatedStatus();
}
}
/*******************************************************************************
** Remove the values from the current & total fields
*******************************************************************************/
@ -107,4 +150,17 @@ public class AsyncJobCallback
AsyncJobManager.getStateProvider().put(new UUIDAndTypeStateKey(jobUUID, StateType.ASYNC_JOB_STATUS), asyncJobStatus);
}
/*******************************************************************************
** Check if the asyncJobStatus had a cancellation requested.
**
** TODO - concern about multiple threads writing this object to a non-in-memory
** state provider, and this value getting lost...
*******************************************************************************/
public boolean wasCancelRequested()
{
return (this.asyncJobStatus.getCancelRequested());
}
}

View File

@ -30,13 +30,14 @@ import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
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.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -44,7 +45,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class AsyncJobManager
{
private static final Logger LOG = LogManager.getLogger(AsyncJobManager.class);
private static final QLogger LOG = QLogger.getLogger(AsyncJobManager.class);
@ -72,8 +73,10 @@ public class AsyncJobManager
try
{
CapturedContext capturedContext = QContext.capture();
CompletableFuture<T> future = CompletableFuture.supplyAsync(() ->
{
QContext.init(capturedContext);
return (runAsyncJob(jobName, asyncJob, uuidAndTypeStateKey, asyncJobStatus));
});
@ -91,7 +94,7 @@ public class AsyncJobManager
}
catch(TimeoutException e)
{
LOG.info("Job going async " + uuidAndTypeStateKey.getUuid());
LOG.debug("Job going async " + uuidAndTypeStateKey.getUuid());
throw (new JobGoingAsyncException(uuidAndTypeStateKey.getUuid().toString()));
}
}
@ -135,11 +138,11 @@ public class AsyncJobManager
Thread.currentThread().setName("Job:" + jobName + ":" + uuidAndTypeStateKey.getUuid().toString().substring(0, 8));
try
{
LOG.info("Starting job " + uuidAndTypeStateKey.getUuid());
LOG.debug("Starting job " + uuidAndTypeStateKey.getUuid());
T result = asyncJob.run(new AsyncJobCallback(uuidAndTypeStateKey.getUuid(), asyncJobStatus));
asyncJobStatus.setState(AsyncJobState.COMPLETE);
getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus);
LOG.info("Completed job " + uuidAndTypeStateKey.getUuid());
LOG.debug("Completed job " + uuidAndTypeStateKey.getUuid());
return (result);
}
catch(Exception e)
@ -153,6 +156,7 @@ public class AsyncJobManager
finally
{
Thread.currentThread().setName(originalThreadName);
QContext.clear();
}
}
@ -183,4 +187,15 @@ public class AsyncJobManager
// return TempFileStateProvider.getInstance();
}
/*******************************************************************************
**
*******************************************************************************/
public void cancelJob(String jobUUID)
{
Optional<AsyncJobStatus> jobStatus = getJobStatus(jobUUID);
jobStatus.ifPresent(asyncJobStatus -> asyncJobStatus.setCancelRequested(true));
}
}

View File

@ -37,6 +37,8 @@ public class AsyncJobStatus implements Serializable
private Integer total;
private Exception caughtException;
private boolean cancelRequested;
/*******************************************************************************
@ -163,4 +165,26 @@ public class AsyncJobStatus implements Serializable
{
this.caughtException = caughtException;
}
/*******************************************************************************
** Getter for cancelRequested
**
*******************************************************************************/
public boolean getCancelRequested()
{
return cancelRequested;
}
/*******************************************************************************
** Setter for cancelRequested
**
*******************************************************************************/
public void setCancelRequested(boolean cancelRequested)
{
this.cancelRequested = cancelRequested;
}
}

View File

@ -0,0 +1,179 @@
/*
* 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.async;
import java.io.Serializable;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
/*******************************************************************************
** Class that knows how to Run an asynchronous job (lambda, supplier) that writes into a
** RecordPipe, with another lambda (consumer) that consumes records from the pipe.
**
** Takes care of the job status monitoring, blocking when the pipe is empty, etc.
*******************************************************************************/
public class AsyncRecordPipeLoop
{
private static final QLogger LOG = QLogger.getLogger(AsyncRecordPipeLoop.class);
private static final int TIMEOUT_AFTER_NO_RECORDS_MS = 10 * 60 * 1000;
private static final int MAX_SLEEP_MS = 1000;
private static final int INIT_SLEEP_MS = 10;
private static final int MIN_RECORDS_TO_CONSUME = 10;
/*******************************************************************************
** Run an async-record-pipe-loop.
**
** @param jobName name for the async job thread
** @param recordLimit optionally, cancel the supplier/job after this number of records.
* e.g., for a preview step.
** @param recordPipe constructed before this call, and used in both of the lambdas
** @param supplier lambda that adds records into the pipe.
* e.g., a query or extract step.
** @param consumer lambda that consumes records from the pipe
* e.g., a transform/load step.
*******************************************************************************/
public int run(String jobName, Integer recordLimit, RecordPipe recordPipe, UnsafeFunction<AsyncJobCallback, ? extends Serializable, QException> supplier, UnsafeSupplier<Integer, QException> consumer) throws QException
{
///////////////////////////////////////////////////
// start the extraction function as an async job //
///////////////////////////////////////////////////
AsyncJobManager asyncJobManager = new AsyncJobManager();
String jobUUID = asyncJobManager.startJob(jobName, supplier::apply);
LOG.debug("Started supplier job [" + jobUUID + "] for record pipe.");
AsyncJobState jobState = AsyncJobState.RUNNING;
AsyncJobStatus asyncJobStatus = null;
int recordCount = 0;
int nextSleepMillis = INIT_SLEEP_MS;
long lastReceivedRecordsAt = System.currentTimeMillis();
long jobStartTime = System.currentTimeMillis();
boolean everCalledConsumer = false;
while(jobState.equals(AsyncJobState.RUNNING))
{
if(recordPipe.countAvailableRecords() < MIN_RECORDS_TO_CONSUME)
{
///////////////////////////////////////////////////////////////
// if the pipe is too empty, sleep to let the producer work. //
// todo - smarter sleep? like get notified vs. sleep? //
///////////////////////////////////////////////////////////////
LOG.trace("Too few records are available in the pipe. Sleeping [" + nextSleepMillis + "] ms to give producer a chance to work");
SleepUtils.sleep(nextSleepMillis, TimeUnit.MILLISECONDS);
nextSleepMillis = Math.min(nextSleepMillis * 2, MAX_SLEEP_MS);
long timeSinceLastReceivedRecord = System.currentTimeMillis() - lastReceivedRecordsAt;
if(timeSinceLastReceivedRecord > TIMEOUT_AFTER_NO_RECORDS_MS)
{
throw (new QException("Job appears to have stopped producing records (last record received " + timeSinceLastReceivedRecord + " ms ago)."));
}
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the pipe has records, consume them. reset the sleep timer so if we sleep again it'll be short. //
////////////////////////////////////////////////////////////////////////////////////////////////////////
lastReceivedRecordsAt = System.currentTimeMillis();
nextSleepMillis = INIT_SLEEP_MS;
everCalledConsumer = true;
recordCount += consumer.get();
LOG.debug(String.format("Processed %,d records so far", recordCount));
if(recordLimit != null && recordCount >= recordLimit)
{
asyncJobManager.cancelJob(jobUUID);
////////////////////////////////////////////////////////////////////////////////////////////////////
// in case the extract function doesn't recognize the cancellation request, //
// tell the pipe to "terminate" - meaning - flush its queue and just noop when given new records. //
// this should prevent anyone writing to such a pipe from potentially filling & blocking. //
////////////////////////////////////////////////////////////////////////////////////////////////////
recordPipe.terminate();
break;
}
}
//////////////////////////////
// refresh the job's status //
//////////////////////////////
Optional<AsyncJobStatus> optionalAsyncJobStatus = asyncJobManager.getJobStatus(jobUUID);
if(optionalAsyncJobStatus.isEmpty())
{
/////////////////////////////////////////////////
// todo - ... maybe some version of try-again? //
/////////////////////////////////////////////////
throw (new QException("Could not get status of job [" + jobUUID + "]"));
}
asyncJobStatus = optionalAsyncJobStatus.get();
jobState = asyncJobStatus.getState();
}
LOG.debug("Job [" + jobUUID + "][" + jobName + "] completed with status: " + asyncJobStatus);
///////////////////////////////////
// propagate errors from the job //
///////////////////////////////////
if(asyncJobStatus != null && asyncJobStatus.getState().equals(AsyncJobState.ERROR))
{
throw (new QException("Job failed with an error", asyncJobStatus.getCaughtException()));
}
///////////////////////////////////////////////////////////////////////////////////////////
// send the final records to the consumer //
// note - we'll only make this "final" call to the consumer if: //
// - there are currently records in the pipe //
// - OR we never called the consumer (e.g., there were 0 rows produced by the supplier //
// This prevents cases where a consumer may get pages of records in the loop, but then //
// be called here post-loop w/ 0 records, and may interpret it as a sign that no records //
// were ever supplied. //
///////////////////////////////////////////////////////////////////////////////////////////
if(recordPipe.countAvailableRecords() > 0 || !everCalledConsumer)
{
recordCount += consumer.get();
}
long endTime = System.currentTimeMillis();
if(recordCount > 0)
{
LOG.info(String.format("Processed %,d records", recordCount)
+ String.format(" at end of job [%s] in %,d ms (%.2f records/second).", jobName, (endTime - jobStartTime), 1000d * (recordCount / (.001d + (endTime - jobStartTime)))));
}
return (recordCount);
}
}

View File

@ -0,0 +1,227 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.audits;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
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.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditOutput;
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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Insert an audit (e.g., the same one) against 1 or more records.
**
** Takes care of managing the foreign key tables.
**
** Enforces that security key values are provided, if the table has any.
*******************************************************************************/
public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput>
{
private static final QLogger LOG = QLogger.getLogger(AuditAction.class);
/*******************************************************************************
**
*******************************************************************************/
public static void execute(String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message)
{
new AuditAction().execute(new AuditInput()
.withAuditTableName(tableName)
.withRecordIdList(List.of(recordId))
.withSecurityKeyValues(securityKeyValues)
.withMessage(message));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public AuditOutput execute(AuditInput input)
{
AuditOutput auditOutput = new AuditOutput();
try
{
QTableMetaData table = QContext.getQInstance().getTable(input.getAuditTableName());
if(table == null)
{
throw (new QException("Requested audit for an unrecognized table name: " + input.getAuditTableName()));
}
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
if(input.getSecurityKeyValues() == null || !input.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
{
throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + input.getAuditTableName()));
}
}
Integer auditTableId = getIdForName("auditTable", input.getAuditTableName());
Integer auditUserId = getIdForName("auditUser", Objects.requireNonNullElse(input.getAuditUserName(), getSessionUserName()));
Instant timestamp = Objects.requireNonNullElse(input.getTimestamp(), Instant.now());
List<QRecord> auditRecords = new ArrayList<>();
for(Integer recordId : input.getRecordIdList())
{
QRecord record = new QRecord()
.withValue("auditTableId", auditTableId)
.withValue("auditUserId", auditUserId)
.withValue("timestamp", timestamp)
.withValue("message", input.getMessage())
.withValue("recordId", recordId);
if(input.getSecurityKeyValues() != null)
{
for(Map.Entry<String, Serializable> entry : input.getSecurityKeyValues().entrySet())
{
record.setValue(entry.getKey(), entry.getValue());
}
}
auditRecords.add(record);
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName("audit");
insertInput.setRecords(auditRecords);
new InsertAction().execute(insertInput);
}
catch(Exception e)
{
LOG.error("Error performing an audit", e);
}
return (auditOutput);
}
/*******************************************************************************
**
*******************************************************************************/
private static String getSessionUserName()
{
QUser user = QContext.getQSession().getUser();
if(user == null)
{
return ("Unknown");
}
return (user.getFullName());
}
/*******************************************************************************
**
*******************************************************************************/
private Integer getIdForName(String tableName, String nameValue) throws QException
{
Integer id = fetchIdFromName(tableName, nameValue);
if(id != null)
{
return id;
}
try
{
LOG.debug("Inserting " + tableName + " named " + nameValue);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(tableName);
QRecord record = new QRecord().withValue("name", nameValue);
if(tableName.equals("auditTable"))
{
QTableMetaData table = QContext.getQInstance().getTable(nameValue);
if(table != null)
{
record.setValue("label", table.getLabel());
}
}
insertInput.setRecords(List.of(record));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
id = insertOutput.getRecords().get(0).getValueInteger("id");
if(id != null)
{
return id;
}
}
catch(Exception e)
{
////////////////////////////////////////////////////////////////////
// assume this may mean a dupe-key - so - try another fetch below //
////////////////////////////////////////////////////////////////////
LOG.debug("Caught error inserting " + tableName + " named " + nameValue + " - will try to re-fetch", e);
}
id = fetchIdFromName(tableName, nameValue);
if(id != null)
{
return id;
}
/////////////
// give up //
/////////////
throw (new QException("Unable to get id for " + tableName + " named " + nameValue));
}
/*******************************************************************************
**
*******************************************************************************/
private static Integer fetchIdFromName(String tableName, String nameValue) throws QException
{
GetInput getInput = new GetInput();
getInput.setTableName(tableName);
getInput.setUniqueKey(Map.of("name", nameValue));
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() != null)
{
return (getOutput.getRecord().getValueInteger("id"));
}
return null;
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.automation;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
/*******************************************************************************
** enum of possible values for a record's Automation Status.
*******************************************************************************/
public enum AutomationStatus implements PossibleValueEnum<Integer>
{
PENDING_INSERT_AUTOMATIONS(1, "Pending Insert Automations"),
RUNNING_INSERT_AUTOMATIONS(2, "Running Insert Automations"),
FAILED_INSERT_AUTOMATIONS(3, "Failed Insert Automations"),
PENDING_UPDATE_AUTOMATIONS(4, "Pending Update Automations"),
RUNNING_UPDATE_AUTOMATIONS(5, "Running Update Automations"),
FAILED_UPDATE_AUTOMATIONS(6, "Failed Update Automations"),
OK(7, "OK");
private final Integer id;
private final String label;
/*******************************************************************************
**
*******************************************************************************/
AutomationStatus(int id, String label)
{
this.id = id;
this.label = label;
}
/*******************************************************************************
** Getter for id
**
*******************************************************************************/
public Integer getId()
{
return (id);
}
/*******************************************************************************
** Getter for label
**
*******************************************************************************/
public String getLabel()
{
return (label);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Integer getPossibleValueId()
{
return (getId());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getPossibleValueLabel()
{
return (getLabel());
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public String getInsertOrUpdate()
{
return switch(this)
{
case PENDING_INSERT_AUTOMATIONS, RUNNING_INSERT_AUTOMATIONS, FAILED_INSERT_AUTOMATIONS -> "Insert";
case PENDING_UPDATE_AUTOMATIONS, RUNNING_UPDATE_AUTOMATIONS, FAILED_UPDATE_AUTOMATIONS -> "Update";
case OK -> "";
};
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.automation;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
/*******************************************************************************
** Base class for custom-codes to run as an automation action
*******************************************************************************/
public abstract class RecordAutomationHandler
{
/*******************************************************************************
**
*******************************************************************************/
public abstract void execute(RecordAutomationInput recordAutomationInput) throws QException;
}

View File

@ -0,0 +1,170 @@
/*
* 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.automation;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
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.update.UpdateInput;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
** Utility class for updating the automation status data for records
*******************************************************************************/
public class RecordAutomationStatusUpdater
{
private static final QLogger LOG = QLogger.getLogger(RecordAutomationStatusUpdater.class);
/*******************************************************************************
** for a list of records from a table, set their automation status - based on
** how the table is configured.
*******************************************************************************/
public static boolean setAutomationStatusInRecords(QSession session, QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus)
{
if(table == null || table.getAutomationDetails() == null || CollectionUtils.nullSafeIsEmpty(records))
{
return (false);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// In case an automation is running, and it updates records - don't let those records be marked //
// as PENDING_UPDATE_AUTOMATIONS... this is meant to avoid having a record's automation update //
// itself, and then continue to do so in a loop (infinitely). //
// BUT - shouldn't this be allowed to update OTHER records to be pending updates? It seems like //
// yes - so -that'll probably be a bug to fix at some point in the future todo //
///////////////////////////////////////////////////////////////////////////////////////////////////
if(automationStatus.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS))
{
Exception e = new Exception();
for(StackTraceElement stackTraceElement : e.getStackTrace())
{
String className = stackTraceElement.getClassName();
if(className.contains("com.kingsrook.qqq.backend.core.actions.automation") && !className.equals(RecordAutomationStatusUpdater.class.getName()) && !className.endsWith("Test"))
{
LOG.debug("Avoiding re-setting automation status to PENDING_UPDATE while running an automation");
return (false);
}
}
}
if(canWeSkipPendingAndGoToOkay(table, automationStatus))
{
automationStatus = AutomationStatus.OK;
}
QTableAutomationDetails automationDetails = table.getAutomationDetails();
if(automationDetails.getStatusTracking() != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
{
for(QRecord record : records)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - seems like there's some case here, where if an order was in PENDING_INSERT, but then some other job updated the record, that we'd //
// lose that pending status, which would be a Bad Thing™... //
// problem is - we may not have the full record in here, so we can't necessarily check the record to see what status it's currently in... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId());
// todo - another field - for the automation timestamp??
}
}
return (true);
}
/*******************************************************************************
** If a table has no automation actions defined for Insert (or Update), and we're
** being asked to set status to PENDING_INSERT (or PENDING_UPDATE), then just
** move the status straight to OK.
*******************************************************************************/
private static boolean canWeSkipPendingAndGoToOkay(QTableMetaData table, AutomationStatus automationStatus)
{
List<TableAutomationAction> tableActions = Objects.requireNonNullElse(table.getAutomationDetails().getActions(), new ArrayList<>());
if(automationStatus.equals(AutomationStatus.PENDING_INSERT_AUTOMATIONS))
{
return tableActions.stream().noneMatch(a -> TriggerEvent.POST_INSERT.equals(a.getTriggerEvent()));
}
else if(automationStatus.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS))
{
return tableActions.stream().noneMatch(a -> TriggerEvent.POST_UPDATE.equals(a.getTriggerEvent()));
}
return (false);
}
/*******************************************************************************
** for a list of records, update their automation status and actually Update the
** backend as well.
*******************************************************************************/
public static void setAutomationStatusInRecordsAndUpdate(QInstance instance, QSession session, QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus) throws QException
{
QTableAutomationDetails automationDetails = table.getAutomationDetails();
if(automationDetails != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
{
boolean didSetStatusField = setAutomationStatusInRecords(session, table, records, automationStatus);
if(didSetStatusField)
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(table.getName());
/////////////////////////////////////////////////////////////////////////////////////
// build records with just their pkey & status field for this update, to avoid //
// changing other values (relies on assumption of Patch semantics in UpdateAction) //
/////////////////////////////////////////////////////////////////////////////////////
updateInput.setRecords(records.stream().map(r -> new QRecord()
.withTableName(r.getTableName())
.withValue(table.getPrimaryKeyField(), r.getValue(table.getPrimaryKeyField()))
.withValue(automationDetails.getStatusTracking().getFieldName(), r.getValue(automationDetails.getStatusTracking().getFieldName()))).toList());
updateInput.setAreAllValuesBeingUpdatedTheSame(true);
new UpdateAction().execute(updateInput);
}
}
else
{
// todo - verify if this is valid as other types are built
throw (new NotImplementedException("Updating record automation status is not implemented for table [" + table + "], tracking type: "
+ (automationDetails == null ? "null" : automationDetails.getStatusTracking().getType())));
}
}
}

View File

@ -0,0 +1,413 @@
/*
* 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.automation.polling;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
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.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
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.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.automation.RecordAutomationInput;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
** Runnable for the Polling Automation Provider, that looks for records that
** need automations, and executes them.
**
** An instance of this class should be created for each table/automation-status
** - see the TableActions inner record for that definition, and the static
** getTableActions method that helps someone who wants to start these threads
** figure out which ones are needed.
*******************************************************************************/
public class PollingAutomationPerTableRunner implements Runnable
{
private static final QLogger LOG = QLogger.getLogger(PollingAutomationPerTableRunner.class);
private final TableActions tableActions;
private final String name;
private QInstance instance;
private Supplier<QSession> sessionSupplier;
private static Map<TriggerEvent, AutomationStatus> triggerEventAutomationStatusMap = Map.of(
TriggerEvent.POST_INSERT, AutomationStatus.PENDING_INSERT_AUTOMATIONS,
TriggerEvent.POST_UPDATE, AutomationStatus.PENDING_UPDATE_AUTOMATIONS
);
private static Map<AutomationStatus, AutomationStatus> pendingToRunningStatusMap = Map.of(
AutomationStatus.PENDING_INSERT_AUTOMATIONS, AutomationStatus.RUNNING_INSERT_AUTOMATIONS,
AutomationStatus.PENDING_UPDATE_AUTOMATIONS, AutomationStatus.RUNNING_UPDATE_AUTOMATIONS
);
private static Map<AutomationStatus, AutomationStatus> pendingToFailedStatusMap = Map.of(
AutomationStatus.PENDING_INSERT_AUTOMATIONS, AutomationStatus.FAILED_INSERT_AUTOMATIONS,
AutomationStatus.PENDING_UPDATE_AUTOMATIONS, AutomationStatus.FAILED_UPDATE_AUTOMATIONS
);
/*******************************************************************************
**
*******************************************************************************/
public record TableActions(String tableName, AutomationStatus status, List<TableAutomationAction> actions)
{
}
/*******************************************************************************
**
*******************************************************************************/
public static List<TableActions> getTableActions(QInstance instance, String providerName)
{
Map<String, Map<AutomationStatus, List<TableAutomationAction>>> workingTableActionMap = new HashMap<>();
List<TableActions> tableActionList = new ArrayList<>();
//////////////////////////////////////////////////////////////////////
// todo - share logic like this among any automation implementation //
//////////////////////////////////////////////////////////////////////
for(QTableMetaData table : instance.getTables().values())
{
if(table.getAutomationDetails() != null && providerName.equals(table.getAutomationDetails().getProviderName()))
{
///////////////////////////////////////////////////////////////////////////
// organize the table's actions by type //
// todo - in future, need user-defined actions here too (and refreshed!) //
///////////////////////////////////////////////////////////////////////////
for(TableAutomationAction action : table.getAutomationDetails().getActions())
{
AutomationStatus automationStatus = triggerEventAutomationStatusMap.get(action.getTriggerEvent());
workingTableActionMap.putIfAbsent(table.getName(), new HashMap<>());
workingTableActionMap.get(table.getName()).putIfAbsent(automationStatus, new ArrayList<>());
workingTableActionMap.get(table.getName()).get(automationStatus).add(action);
}
////////////////////////////////////////////
// convert the map to tableAction records //
////////////////////////////////////////////
for(Map.Entry<AutomationStatus, List<TableAutomationAction>> entry : workingTableActionMap.get(table.getName()).entrySet())
{
AutomationStatus automationStatus = entry.getKey();
List<TableAutomationAction> actionList = entry.getValue();
actionList.sort(Comparator.comparing(TableAutomationAction::getPriority));
tableActionList.add(new TableActions(table.getName(), automationStatus, actionList));
}
}
}
return (tableActionList);
}
/*******************************************************************************
**
*******************************************************************************/
public PollingAutomationPerTableRunner(QInstance instance, String providerName, Supplier<QSession> sessionSupplier, TableActions tableActions)
{
this.instance = instance;
this.sessionSupplier = sessionSupplier;
this.tableActions = tableActions;
this.name = providerName + ">" + tableActions.tableName() + ">" + tableActions.status().getInsertOrUpdate();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run()
{
QContext.init(instance, sessionSupplier.get());
String originalThreadName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
LOG.info("Running " + this.getClass().getSimpleName() + "[" + name + "]");
try
{
QSession session = sessionSupplier != null ? sessionSupplier.get() : new QSession();
processTableInsertOrUpdate(instance.getTable(tableActions.tableName()), session, tableActions.status(), tableActions.actions());
}
catch(Exception e)
{
LOG.warn("Error running automations", e);
}
finally
{
Thread.currentThread().setName(originalThreadName);
QContext.clear();
}
}
/*******************************************************************************
** Query for and process records that have a PENDING_INSERT or PENDING_UPDATE status on a given table.
*******************************************************************************/
public void processTableInsertOrUpdate(QTableMetaData table, QSession session, AutomationStatus automationStatus, List<TableAutomationAction> actions) throws QException
{
if(CollectionUtils.nullSafeIsEmpty(actions))
{
return;
}
LOG.info(" Query for records " + automationStatus + " in " + table);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// run an async-pipe loop - that will query for records in PENDING - put them in a pipe - then apply actions to them //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QTableAutomationDetails automationDetails = table.getAutomationDetails();
AsyncRecordPipeLoop asyncRecordPipeLoop = new AsyncRecordPipeLoop();
RecordPipe recordPipe = automationDetails.getOverrideBatchSize() == null
? new RecordPipe() : new RecordPipe(automationDetails.getOverrideBatchSize());
asyncRecordPipeLoop.run("PollingAutomationRunner>Query>" + automationStatus + ">" + table.getName(), null, recordPipe, (status) ->
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
AutomationStatusTrackingType statusTrackingType = automationDetails.getStatusTracking().getType();
if(AutomationStatusTrackingType.FIELD_IN_TABLE.equals(statusTrackingType))
{
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(automationDetails.getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId()))));
}
else
{
throw (new NotImplementedException("Automation Status Tracking type [" + statusTrackingType + "] is not yet implemented in here."));
}
queryInput.setRecordPipe(recordPipe);
return (new QueryAction().execute(queryInput));
}, () ->
{
List<QRecord> records = recordPipe.consumeAvailableRecords();
applyActionsToRecords(session, table, records, actions, automationStatus);
return (records.size());
}
);
}
/*******************************************************************************
** For a set of records that were found to be in a PENDING state - run all the
** table's actions against them - IF they are found to match the action's filter
** (assuming it has one - if it doesn't, then all records match).
*******************************************************************************/
private void applyActionsToRecords(QSession session, QTableMetaData table, List<QRecord> records, List<TableAutomationAction> actions, AutomationStatus automationStatus) throws QException
{
if(CollectionUtils.nullSafeIsEmpty(records))
{
return;
}
///////////////////////////////////////////////////
// mark the records as RUNNING their automations //
///////////////////////////////////////////////////
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, pendingToRunningStatusMap.get(automationStatus));
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// foreach action - run it against the records (but only if they match the action's filter, if there is one) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
boolean anyActionsFailed = false;
for(TableAutomationAction action : actions)
{
try
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(session, table, records, action);
LOG.info("Of the {} records that were pending automations, {} of them match the filter on the action {}", records.size(), matchingQRecords.size(), action);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.info(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
applyActionToMatchingRecords(instance, session, table, matchingQRecords, action);
}
}
catch(Exception e)
{
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
anyActionsFailed = true;
}
}
////////////////////////////////////////
// update status on all these records //
////////////////////////////////////////
if(anyActionsFailed)
{
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, pendingToFailedStatusMap.get(automationStatus));
}
else
{
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, AutomationStatus.OK);
}
}
/*******************************************************************************
** For a given action, and a list of records - return a new list, of the ones
** which match the action's filter (if there is one - if not, then all match).
**
** Note that this WILL re-query the objects (ALWAYS - even if the action has no filter).
** This has the nice side effect of always giving fresh/updated records, despite having
** some cost.
**
** At one point, we considered just applying the filter using java-comparisons,
** but that will almost certainly give potentially different results than a true
** backend - e.g., just consider if the DB is case-sensitive for strings...
*******************************************************************************/
private List<QRecord> getRecordsMatchingActionFilter(QSession session, QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
QQueryFilter filter = new QQueryFilter();
/////////////////////////////////////////////////////////////////////////////////////////////////////
// copy filter criteria from the action's filter to a new filter that we'll run here. //
// Critically - don't modify the filter object on the action! as that object has a long lifespan. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
if(action.getFilter() != null)
{
if(action.getFilter().getCriteria() != null)
{
action.getFilter().getCriteria().forEach(filter::addCriteria);
}
if(action.getFilter().getOrderBys() != null)
{
action.getFilter().getOrderBys().forEach(filter::addOrderBy);
}
}
filter.addCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, records.stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList()));
/////////////////////////////////////////////////////////////////////////////////////////////
// always add order-by the primary key, to give more predictable/consistent results //
// todo - in future - if this becomes a source of slowness, make this a config to opt-out? //
/////////////////////////////////////////////////////////////////////////////////////////////
filter.addOrderBy(new QFilterOrderBy().withFieldName(table.getPrimaryKeyField()));
queryInput.setFilter(filter);
return (new QueryAction().execute(queryInput).getRecords());
}
/*******************************************************************************
** Finally, actually run action code against a list of known matching records.
** todo not commit - move to somewhere genericer
*******************************************************************************/
public static void applyActionToMatchingRecords(QInstance instance, QSession session, QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws Exception
{
if(StringUtils.hasContent(action.getProcessName()))
{
/////////////////////////////////////////////////////////////////////////////////////////
// if the action has a process associated with it - run that process. //
// tell it to SKIP frontend steps. //
// give the process a callback w/ a query filter that has the p-keys of these records. //
/////////////////////////////////////////////////////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(action.getProcessName());
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
runProcessInput.setCallback(new QProcessCallback()
{
@Override
public QQueryFilter getQueryFilter()
{
List<Serializable> recordIds = records.stream().map(r -> r.getValueInteger(table.getPrimaryKeyField())).collect(Collectors.toList());
return (new QQueryFilter().withCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordIds)));
}
});
RunProcessAction runProcessAction = new RunProcessAction();
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
if(runProcessOutput.getException().isPresent())
{
throw (runProcessOutput.getException().get());
}
}
else if(action.getCodeReference() != null)
{
LOG.debug(" Executing action: [" + action.getName() + "] as code reference: " + action.getCodeReference());
RecordAutomationInput input = new RecordAutomationInput();
input.setTableName(table.getName());
input.setRecordList(records);
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getRecordAutomationHandler(action);
recordAutomationHandler.execute(input);
}
}
/*******************************************************************************
** Getter for name
**
*******************************************************************************/
public String getName()
{
return name;
}
}

View File

@ -0,0 +1,65 @@
/*
* 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.customizers;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractPostInsertCustomizer
{
protected InsertInput insertInput;
/*******************************************************************************
**
*******************************************************************************/
public abstract List<QRecord> apply(List<QRecord> records);
/*******************************************************************************
** Getter for insertInput
**
*******************************************************************************/
public InsertInput getInsertInput()
{
return insertInput;
}
/*******************************************************************************
** Setter for insertInput
**
*******************************************************************************/
public void setInsertInput(InsertInput insertInput)
{
this.insertInput = insertInput;
}
}

View File

@ -0,0 +1,65 @@
/*
* 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.customizers;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractPostQueryCustomizer
{
protected AbstractTableActionInput input;
/*******************************************************************************
**
*******************************************************************************/
public abstract List<QRecord> apply(List<QRecord> records);
/*******************************************************************************
** Getter for input
**
*******************************************************************************/
public AbstractTableActionInput getInput()
{
return (input);
}
/*******************************************************************************
** Setter for input
**
*******************************************************************************/
public void setInput(AbstractTableActionInput input)
{
this.input = input;
}
}

View File

@ -0,0 +1,160 @@
/*
* 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.customizers;
import java.io.Serializable;
import java.util.ArrayList;
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.exceptions.QException;
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.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Standard/re-usable post-insert customizer, for the use case where, when we
** do an insert into table "parent", we want a record automatically inserted into
** table "child", and there's a foreign key in "parent", pointed at "child"
** e.g., named: "parent.childId".
**
** A similar use-case would have the foreign key in the child table - in which case,
** we could add a "Type" enum, plus abstract method to get our "Type", then logic
** to switch behavior based on type. See existing type enum, but w/ only 1 case :)
*******************************************************************************/
public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInsertCustomizer
{
public enum RelationshipType
{
PARENT_POINTS_AT_CHILD
}
/*******************************************************************************
**
*******************************************************************************/
public abstract QRecord buildChildForRecord(QRecord parentRecord) throws QException;
/*******************************************************************************
**
*******************************************************************************/
public abstract String getChildTableName();
/*******************************************************************************
**
*******************************************************************************/
public abstract String getForeignKeyFieldName();
/*******************************************************************************
**
*******************************************************************************/
public abstract RelationshipType getRelationshipType();
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> apply(List<QRecord> records)
{
try
{
List<QRecord> rs = new ArrayList<>();
List<QRecord> childrenToInsert = new ArrayList<>();
QTableMetaData table = getInsertInput().getTable();
QTableMetaData childTable = getInsertInput().getInstance().getTable(getChildTableName());
////////////////////////////////////////////////////////////////////////////////
// iterate over the inserted records, building a list child records to insert //
// for ones missing a value in the foreign key field. //
////////////////////////////////////////////////////////////////////////////////
for(QRecord record : records)
{
if(record.getValue(getForeignKeyFieldName()) == null)
{
childrenToInsert.add(buildChildForRecord(record));
}
}
///////////////////////////////////////////////////////////////////////////////////
// if there are no children to insert, then just return the original record list //
///////////////////////////////////////////////////////////////////////////////////
if(childrenToInsert.isEmpty())
{
return (records);
}
/////////////////////////
// insert the children //
/////////////////////////
InsertInput insertInput = new InsertInput();
insertInput.setTableName(getChildTableName());
insertInput.setRecords(childrenToInsert);
insertInput.setTransaction(this.insertInput.getTransaction());
InsertOutput insertOutput = new InsertAction().execute(insertInput);
Iterator<QRecord> insertedRecordIterator = insertOutput.getRecords().iterator();
//////////////////////////////////////////////////////////////////////////////////////////////////////
// iterate over the original list of records again - for any that need a child (e.g., are missing //
// foreign key), set their foreign key to a newly inserted child's key, and add them to be updated. //
//////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> recordsToUpdate = new ArrayList<>();
for(QRecord record : records)
{
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
if(record.getValue(getForeignKeyFieldName()) == null)
{
Serializable foreignKey = insertedRecordIterator.next().getValue(childTable.getPrimaryKeyField());
recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), primaryKey).withValue(getForeignKeyFieldName(), foreignKey));
record.setValue(getForeignKeyFieldName(), foreignKey);
rs.add(record);
}
else
{
rs.add(record);
}
}
////////////////////////////////////////////////////////////////////////////
// update the originally inserted records to reference their new children //
////////////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(getInsertInput().getTableName());
updateInput.setRecords(recordsToUpdate);
updateInput.setTransaction(this.insertInput.getTransaction());
new UpdateAction().execute(updateInput);
return (rs);
}
catch(Exception e)
{
throw new RuntimeException("Error inserting new child records for new parent records", e);
}
}
}

View File

@ -24,14 +24,16 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
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.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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
/*******************************************************************************
@ -39,7 +41,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QCodeLoader
{
private static final Logger LOG = LogManager.getLogger(QCodeLoader.class);
private static final QLogger LOG = QLogger.getLogger(QCodeLoader.class);
@ -58,6 +60,21 @@ public class QCodeLoader
/*******************************************************************************
**
*******************************************************************************/
public static <T> Optional<T> getTableCustomizer(Class<T> expectedClass, QTableMetaData table, String customizerName)
{
Optional<QCodeReference> codeReference = table.getCustomizer(customizerName);
if(codeReference.isPresent())
{
return (Optional.ofNullable(QCodeLoader.getAdHoc(expectedClass, codeReference.get())));
}
return (Optional.empty());
}
/*******************************************************************************
**
*******************************************************************************/
@ -97,6 +114,120 @@ public class QCodeLoader
/*******************************************************************************
**
*******************************************************************************/
@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: " + 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> T getAdHoc(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 code references 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: " + 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);
}
}
/*******************************************************************************
**
*******************************************************************************/
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));
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -121,4 +252,5 @@ public class QCodeLoader
throw (new QException("Error getting custom possible value provider for PVS [" + possibleValueSource.getName() + "]", e));
}
}
}

View File

@ -1,3 +1,24 @@
/*
* 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.customizers;
@ -28,6 +49,18 @@ public class TableCustomizer
/*******************************************************************************
**
*******************************************************************************/
public TableCustomizer(String role, Class<?> expectedType)
{
this.role = role;
this.expectedType = expectedType;
this.validationFunction = null;
}
/*******************************************************************************
** Getter for role
**

View File

@ -1,10 +1,27 @@
/*
* 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.customizers;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Enum definition of possible table customizers - "roles" for custom code that
** can be applied to tables.
@ -22,11 +39,8 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
*******************************************************************************/
public enum TableCustomizers
{
POST_QUERY_RECORD(new TableCustomizer("postQueryRecord", Function.class, ((Object x) ->
{
Function<QRecord, QRecord> function = (Function<QRecord, QRecord>) x;
QRecord output = function.apply(new QRecord());
})));
POST_QUERY_RECORD(new TableCustomizer("postQueryRecord", AbstractPostQueryCustomizer.class)),
POST_INSERT_RECORD(new TableCustomizer("postInsertRecord", AbstractPostInsertCustomizer.class));
private final TableCustomizer tableCustomizer;

View File

@ -0,0 +1,269 @@
/*
* 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;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Base class for rendering qqq HTML dashboard widgets
**
*******************************************************************************/
public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
protected String openTopLevelBulletList()
{
return ("""
<div style="padding-left: 2rem;">
<ul>""");
}
/*******************************************************************************
**
*******************************************************************************/
protected String closeTopLevelBulletList()
{
return ("""
</ul>
</div>""");
}
/*******************************************************************************
**
*******************************************************************************/
protected String bulletItalics(String text)
{
return ("<li><i>" + text + "</i></li>");
}
/*******************************************************************************
**
*******************************************************************************/
protected String bulletLink(String href, String text)
{
return ("<li><a href=\"" + href + "\">" + text + "</a></li>");
}
/*******************************************************************************
**
*******************************************************************************/
protected String bulletNameLink(String name, String href, String text)
{
return (bulletNameValue(name, "<a href=\"" + href + "\">" + text + "</a>"));
}
/*******************************************************************************
**
*******************************************************************************/
protected String bulletNameValue(String name, String value)
{
return ("<li><b>" + name + "</b> &nbsp; " + Objects.requireNonNullElse(value, "--") + "</li>");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableBulkLoad(RenderWidgetInput input, String tableName) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
return (tablePath + "/" + tableName + ".bulkInsert");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableBulkLoadChildren(String tableName) throws QException
{
return ("#/launchProcess=" + tableName + ".bulkInsert");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableCreate(RenderWidgetInput input, String tableName) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
return (tablePath + "/create");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableCreateWithDefaultValues(RenderWidgetInput input, String tableName, Map<String, Serializable> defaultValues) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
return (tablePath + "/create?defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), Charset.defaultCharset()));
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableFilter(RenderWidgetInput input, String tableName, QQueryFilter filter) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
}
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefTableFilterNoOfRecords(RenderWidgetInput input, String tableName, QQueryFilter filter, Integer noOfRecords, String singularLabel, String pluralLabel) throws QException
{
String href = linkTableFilter(input, tableName, filter);
return ("<a href=\"" + href + "\">" + QValueFormatter.formatValue(DisplayFormat.COMMAS, noOfRecords) + " " + StringUtils.plural(noOfRecords, singularLabel, pluralLabel) + "</a>");
}
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefViewRecord(RenderWidgetInput input, String tableName, Serializable id, String linkText) throws QException
{
return ("<a href=\"" + linkRecordView(input, tableName, id) + "\">" + linkText + "</a>");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkRecordEdit(AbstractActionInput input, String tableName, Serializable recordId) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
return (tablePath + "/" + recordId + "/edit");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkRecordView(AbstractActionInput input, String tableName, Serializable recordId) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
return (tablePath + "/" + recordId);
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkProcessForRecord(AbstractActionInput input, String processName, Serializable recordId) throws QException
{
QProcessMetaData process = input.getInstance().getProcess(processName);
String tableName = process.getTableName();
String tablePath = input.getInstance().getTablePath(input, tableName);
return (tablePath + "/" + recordId + "/" + processName);
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableCreateChild(String childTableName, Map<String, Serializable> defaultValues)
{
return (linkTableCreateChild(childTableName, defaultValues, defaultValues.keySet()));
}
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefTableCreateChild(String childTableName, Map<String, Serializable> defaultValues)
{
return (aHrefTableCreateChild(childTableName, defaultValues, defaultValues.keySet()));
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableCreateChild(String childTableName, Map<String, Serializable> defaultValues, Set<String> disabledFields)
{
Map<String, Integer> disabledFieldsMap = disabledFields.stream().collect(Collectors.toMap(k -> k, k -> 1));
return ("#/createChild=" + childTableName
+ "/defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), StandardCharsets.UTF_8).replaceAll("\\+", "%20")
+ "/disabledFields=" + URLEncoder.encode(JsonUtils.toJson(disabledFieldsMap), StandardCharsets.UTF_8).replaceAll("\\+", "%20"));
}
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefTableCreateChild(String childTableName, Map<String, Serializable> defaultValues, Set<String> disabledFields)
{
return ("<a href=\"" + linkTableCreateChild(childTableName, defaultValues, defaultValues.keySet()) + "\">Create new</a>");
}
}

View File

@ -0,0 +1,62 @@
/*
* 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;
import java.io.Serializable;
import java.util.Map;
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.AbstractWidgetRenderer;
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;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Class for loading widget implementation code and rendering of widgets
**
*******************************************************************************/
public class RenderWidgetAction
{
/*******************************************************************************
**
*******************************************************************************/
public RenderWidgetOutput execute(RenderWidgetInput input) throws QException
{
ActionHelper.validateSession(input);
AbstractWidgetRenderer widgetRenderer = QCodeLoader.getAdHoc(AbstractWidgetRenderer.class, input.getWidgetMetaData().getCodeReference());
///////////////////////////////////////////////////////////////
// move default values from meta data into this render input //
///////////////////////////////////////////////////////////////
for(Map.Entry<String, Serializable> entry : input.getWidgetMetaData().getDefaultValues().entrySet())
{
input.addQueryParam(entry.getKey(), ValueUtils.getValueAsString(entry.getValue()));
}
return (widgetRenderer.render(input));
}
}

View File

@ -0,0 +1,166 @@
/*
* 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.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
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.exceptions.QException;
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.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput;
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.QWidgetData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.WidgetDropdownData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Base class for rendering qqq dashboard widgets
**
*******************************************************************************/
public abstract class AbstractWidgetRenderer
{
public static final QValueFormatter valueFormatter = new QValueFormatter();
public static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault());
/*******************************************************************************
**
*******************************************************************************/
public abstract RenderWidgetOutput render(RenderWidgetInput input) throws QException;
/*******************************************************************************
**
*******************************************************************************/
protected boolean setupDropdowns(RenderWidgetInput input, QWidgetMetaData metaData, QWidgetData widgetData) throws QException
{
List<List<Map<String, String>>> pvsData = new ArrayList<>();
List<String> pvsLabels = new ArrayList<>();
List<String> pvsNames = new ArrayList<>();
List<String> missingRequiredSelections = new ArrayList<>();
for(WidgetDropdownData dropdownData : CollectionUtils.nonNullList(metaData.getDropdowns()))
{
String possibleValueSourceName = dropdownData.getPossibleValueSourceName();
QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this looks complicated, but is just look for a label in the dropdown data and if found use it, //
// otherwise look for label in PVS and if found use that, otherwise just use the PVS name //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
String pvsLabel = dropdownData.getLabel() != null ? dropdownData.getLabel() : (possibleValueSource.getLabel() != null ? possibleValueSource.getLabel() : possibleValueSourceName);
pvsLabels.add(pvsLabel);
pvsNames.add(possibleValueSourceName);
SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput();
pvsInput.setPossibleValueSourceName(possibleValueSourceName);
if(dropdownData.getForeignKeyFieldName() != null)
{
////////////////////////////////////////
// look for an id in the query params //
////////////////////////////////////////
Integer id = null;
if(input.getQueryParams() != null && input.getQueryParams().containsKey("id") && StringUtils.hasContent(input.getQueryParams().get("id")))
{
id = Integer.parseInt(input.getQueryParams().get("id"));
}
if(id != null)
{
pvsInput.setDefaultQueryFilter(new QQueryFilter().withCriteria(
new QFilterCriteria(
dropdownData.getForeignKeyFieldName(),
QCriteriaOperator.EQUALS,
id)));
}
}
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(pvsInput);
List<Map<String, String>> dropdownOptionList = new ArrayList<>();
pvsData.add(dropdownOptionList);
//////////////////////////////////////////
// sort results, dedupe, and add to map //
//////////////////////////////////////////
Set<String> exists = new HashSet<>();
output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel()));
output.getResults().sort(Comparator.comparing(QPossibleValue::getLabel));
for(QPossibleValue<?> possibleValue : output.getResults())
{
dropdownOptionList.add(Map.of(
"id", String.valueOf(possibleValue.getId()),
"label", possibleValue.getLabel()
));
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// because we know the dropdowns and what the field names will be when something is selected, we can make //
// sure that something has been selected, and if not, display a message that a selection needs made //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(dropdownData.getIsRequired())
{
if(!input.getQueryParams().containsKey(possibleValueSourceName) || !StringUtils.hasContent(input.getQueryParams().get(possibleValueSourceName)))
{
missingRequiredSelections.add(pvsLabel);
}
}
}
widgetData.setDropdownNameList(pvsNames);
widgetData.setDropdownLabelList(pvsLabels);
widgetData.setDropdownDataList(pvsData);
////////////////////////////////////////////////////////////////////////////////
// if there are any missing required dropdowns, build up a message to display //
////////////////////////////////////////////////////////////////////////////////
if(missingRequiredSelections.size() > 0)
{
StringBuilder sb = new StringBuilder("Please select a ").append(StringUtils.joinWithCommasAndAnd(missingRequiredSelections));
sb.append(" from the ").append(StringUtils.plural(missingRequiredSelections.size(), "dropdown", "dropdowns")).append(" above.");
widgetData.setDropdownNeedsSelectedText(sb.toString());
return (false);
}
else
{
return (true);
}
}
}

View File

@ -0,0 +1,217 @@
/*
* 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.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
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.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.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.data.QRecord;
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.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.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
/*******************************************************************************
** Generic widget for display a list of child records.
*******************************************************************************/
public class ChildRecordListRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
public static Builder widgetMetaDataBuilder(QJoinMetaData join)
{
return (new Builder(new QWidgetMetaData()
.withName(join.getName())
.withIsCard(true)
.withCodeReference(new QCodeReference(ChildRecordListRenderer.class, null))
.withType(WidgetType.CHILD_RECORD_LIST.getType())
.withDefaultValue("joinName", join.getName())));
}
/*******************************************************************************
**
*******************************************************************************/
public static class Builder extends AbstractWidgetMetaDataBuilder
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Builder(QWidgetMetaData widgetMetaData)
{
super(widgetMetaData);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withName(String name)
{
widgetMetaData.setName(name);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withLabel(String label)
{
widgetMetaData.setLabel(label);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withCanAddChildRecord(boolean b)
{
widgetMetaData.withDefaultValue("canAddChildRecord", true);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withDisabledFieldsForNewChildRecords(Set<String> disabledFieldsForNewChildRecords)
{
widgetMetaData.withDefaultValue("disabledFieldsForNewChildRecords", new HashSet<>(disabledFieldsForNewChildRecords));
return (this);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
String widgetLabel = input.getQueryParams().get("widgetLabel");
String joinName = input.getQueryParams().get("joinName");
QJoinMetaData join = input.getInstance().getJoin(joinName);
String id = input.getQueryParams().get("id");
////////////////////////////////////////////////////////
// fetch the record that we're getting children for. //
// e.g., the left-side of the join, with the input id //
////////////////////////////////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(join.getLeftTable());
getInput.setPrimaryKey(id);
GetOutput getOutput = new GetAction().execute(getInput);
QRecord record = getOutput.getRecord();
if(record == null)
{
QTableMetaData table = input.getInstance().getTable(join.getLeftTable());
throw (new QNotFoundException("Could not find " + (table == null ? "" : table.getLabel()) + " with primary key " + id));
}
////////////////////////////////////////////////////////////////////
// set up the query - for the table on the right side of the join //
////////////////////////////////////////////////////////////////////
QQueryFilter filter = new QQueryFilter();
for(JoinOn joinOn : join.getJoinOns())
{
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(record.getValue(joinOn.getLeftField()))));
}
filter.setOrderBys(join.getOrderBys());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(join.getRightTable());
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
QTableMetaData table = input.getInstance().getTable(join.getRightTable());
String tablePath = input.getInstance().getTablePath(input, table.getName());
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, table, tablePath, viewAllLink);
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
{
widgetData.setCanAddChildRecord(true);
//////////////////////////////////////////////////////////
// new child records must have values from the join-ons //
//////////////////////////////////////////////////////////
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
for(JoinOn joinOn : join.getJoinOns())
{
defaultValuesForNewChildRecords.put(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
}
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
{
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
}
}
return (new RenderWidgetOutput(widgetData));
}
}

View File

@ -0,0 +1,92 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.util.Map;
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;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
/*******************************************************************************
**
*******************************************************************************/
public class DefaultWidgetRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
return new RenderWidgetOutput(new DefaultWidgetData(input));
}
/*******************************************************************************
**
*******************************************************************************/
public static class DefaultWidgetData extends QWidgetData
{
private final String type;
private final Map<String, String> queryParams;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public DefaultWidgetData(RenderWidgetInput renderWidgetInput)
{
this.type = renderWidgetInput.getWidgetMetaData().getType();
this.queryParams = renderWidgetInput.getQueryParams();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getType()
{
return (type);
}
/*******************************************************************************
** Getter for queryParams
**
*******************************************************************************/
public Map<String, String> getQueryParams()
{
return queryParams;
}
}
}

View File

@ -0,0 +1,47 @@
/*
* 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 com.kingsrook.qqq.backend.core.actions.ActionHelper;
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;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.DividerWidgetData;
/*******************************************************************************
** Generic widget for showing a divider
*******************************************************************************/
public class DividerWidgetRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
ActionHelper.validateSession(input);
return (new RenderWidgetOutput(new DividerWidgetData()));
}
}

View File

@ -0,0 +1,71 @@
/*
* 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 com.kingsrook.qqq.backend.core.actions.ActionHelper;
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;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ParentWidgetData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaData;
/*******************************************************************************
** Generic widget for display a parent widget with children of possible values,
** child widgets, and child actions
*******************************************************************************/
public class ParentWidgetRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
ActionHelper.validateSession(input);
try
{
ParentWidgetMetaData metaData = (ParentWidgetMetaData) input.getWidgetMetaData();
ParentWidgetData widgetData = new ParentWidgetData();
/////////////////////////////////////////////////////////////
// handle any PVSs creating dropdown data for the frontend //
/////////////////////////////////////////////////////////////
boolean dropdownsValid = setupDropdowns(input, metaData, widgetData);
if(dropdownsValid)
{
widgetData.setChildWidgetNameList(metaData.getChildWidgetNameList());
}
return (new RenderWidgetOutput(widgetData));
}
catch(Exception e)
{
throw (new QException("Error rendering parent widget", e));
}
}
}

View File

@ -0,0 +1,73 @@
/*
* 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.util.HashMap;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
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;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ProcessWidgetData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
/*******************************************************************************
** Generic widget for displaying a process as a widget
*******************************************************************************/
public class ProcessWidgetRenderer extends AbstractWidgetRenderer
{
public static final String WIDGET_PROCESS_NAME = "processName";
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
ActionHelper.validateSession(input);
try
{
ProcessWidgetData data = new ProcessWidgetData();
if(input.getWidgetMetaData() instanceof QWidgetMetaData widgetMetaData)
{
setupDropdowns(input, widgetMetaData, data);
String processName = (String) widgetMetaData.getDefaultValues().get(WIDGET_PROCESS_NAME);
QProcessMetaData processMetaData = input.getInstance().getProcess(processName);
data.setProcessMetaData(processMetaData);
data.setDefaultValues(new HashMap<>(input.getQueryParams()));
}
return (new RenderWidgetOutput(data));
}
catch(Exception e)
{
throw (new QException("Error rendering process widget", e));
}
}
}

View File

@ -0,0 +1,104 @@
/*
* 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 com.kingsrook.qqq.backend.core.actions.ActionHelper;
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;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QuickSightChart;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QuickSightChartMetaData;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.quicksight.QuickSightClient;
import software.amazon.awssdk.services.quicksight.model.GenerateEmbedUrlForRegisteredUserRequest;
import software.amazon.awssdk.services.quicksight.model.GenerateEmbedUrlForRegisteredUserResponse;
import software.amazon.awssdk.services.quicksight.model.RegisteredUserDashboardEmbeddingConfiguration;
import software.amazon.awssdk.services.quicksight.model.RegisteredUserEmbeddingExperienceConfiguration;
/*******************************************************************************
** Widget implementation for amazon QuickSight charts
**
*******************************************************************************/
public class QuickSightChartRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
ActionHelper.validateSession(input);
try
{
QuickSightChartMetaData quickSightMetaData = (QuickSightChartMetaData) input.getWidgetMetaData();
QuickSightClient quickSightClient = getQuickSightClient(quickSightMetaData);
final RegisteredUserEmbeddingExperienceConfiguration experienceConfiguration = RegisteredUserEmbeddingExperienceConfiguration.builder()
.dashboard(
RegisteredUserDashboardEmbeddingConfiguration.builder()
.initialDashboardId(quickSightMetaData.getDashboardId())
.build())
.build();
final GenerateEmbedUrlForRegisteredUserRequest generateEmbedUrlForRegisteredUserRequest = GenerateEmbedUrlForRegisteredUserRequest.builder()
.awsAccountId(quickSightMetaData.getAccountId())
.userArn(quickSightMetaData.getUserArn())
.experienceConfiguration(experienceConfiguration)
.build();
final GenerateEmbedUrlForRegisteredUserResponse generateEmbedUrlForRegisteredUserResponse = quickSightClient.generateEmbedUrlForRegisteredUser(generateEmbedUrlForRegisteredUserRequest);
String embedUrl = generateEmbedUrlForRegisteredUserResponse.embedUrl();
QWidgetData widget = new QuickSightChart(input.getWidgetMetaData().getName(), quickSightMetaData.getLabel(), embedUrl);
return (new RenderWidgetOutput(widget));
}
catch(Exception e)
{
throw (new QException("Error rendering widget", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private QuickSightClient getQuickSightClient(QuickSightChartMetaData metaData)
{
AwsBasicCredentials awsCredentials = AwsBasicCredentials.create(metaData.getAccessKey(), metaData.getSecretKey());
QuickSightClient amazonQuickSightClient = QuickSightClient.builder()
.credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
.region(Region.of(metaData.getRegion()))
.build();
return (amazonQuickSightClient);
}
}

View File

@ -0,0 +1,62 @@
/*
* 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.math.BigDecimal;
import java.util.List;
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;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.USMapWidgetData;
/*******************************************************************************
** Generic widget for display a map of the us
*******************************************************************************/
public class USMapWidgetRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
return (new RenderWidgetOutput(
new USMapWidgetData()
.withHeight("250px")
.withMapMarkerList(generateMapMarkerList())
));
}
/*******************************************************************************
**
*******************************************************************************/
protected List<USMapWidgetData.MapMarker> generateMapMarkerList() throws QException
{
return (List.of(new USMapWidgetData.MapMarker("maryville", new BigDecimal("38.725278"), new BigDecimal("-89.957778"))));
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.interfaces;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
/*******************************************************************************
** Interface for the Aggregate action.
**
*******************************************************************************/
public interface AggregateInterface
{
/*******************************************************************************
**
*******************************************************************************/
AggregateOutput execute(AggregateInput aggregateInput) throws QException;
}

View File

@ -0,0 +1,73 @@
/*
* 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.interfaces;
import java.util.HashSet;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
/*******************************************************************************
** Interface for the Get action.
**
*******************************************************************************/
public interface GetInterface
{
/*******************************************************************************
**
*******************************************************************************/
GetOutput execute(GetInput getInput) throws QException;
/*******************************************************************************
**
*******************************************************************************/
default void validateInput(GetInput getInput) throws QException
{
if(getInput.getPrimaryKey() != null & getInput.getUniqueKey() != null)
{
throw new QException("A GetInput may not contain both a primary key [" + getInput.getPrimaryKey() + "] and unique key [" + getInput.getUniqueKey() + "]");
}
if(getInput.getUniqueKey() != null)
{
QTableMetaData table = getInput.getTable();
boolean foundMatch = false;
for(UniqueKey uniqueKey : table.getUniqueKeys())
{
if(new HashSet<>(uniqueKey.getFieldNames()).equals(getInput.getUniqueKey().keySet()))
{
foundMatch = true;
break;
}
}
if(!foundMatch)
{
throw new QException("Table [" + table.getName() + "] does not have a unique key defined on fields: " + getInput.getUniqueKey().keySet().stream().sorted().toList());
}
}
}
}

View File

@ -22,7 +22,6 @@
package com.kingsrook.qqq.backend.core.actions.interfaces;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
@ -32,19 +31,11 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
** Interface for the Insert action.
**
*******************************************************************************/
public interface InsertInterface
public interface InsertInterface extends QActionInterface
{
/*******************************************************************************
**
*******************************************************************************/
InsertOutput execute(InsertInput insertInput) throws QException;
/*******************************************************************************
**
*******************************************************************************/
default QBackendTransaction openTransaction(InsertInput insertInput) throws QException
{
return (new QBackendTransaction());
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.interfaces;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
/*******************************************************************************
**
*******************************************************************************/
public interface QActionInterface
{
/*******************************************************************************
**
*******************************************************************************/
default QBackendTransaction openTransaction(AbstractTableActionInput input) throws QException
{
return (new QBackendTransaction());
}
}

View File

@ -27,17 +27,26 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
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.QBackendMetaData;
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;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
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.permissions.MetaDataWithPermissionRules;
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;
/*******************************************************************************
@ -64,8 +73,18 @@ public class MetaDataAction
Map<String, QFrontendTableMetaData> tables = new LinkedHashMap<>();
for(Map.Entry<String, QTableMetaData> entry : metaDataInput.getInstance().getTables().entrySet())
{
tables.put(entry.getKey(), new QFrontendTableMetaData(entry.getValue(), false));
treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
String tableName = entry.getKey();
QTableMetaData table = entry.getValue();
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, table);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName);
tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false));
treeNodes.put(tableName, new AppTreeNode(table));
}
metaDataOutput.setTables(tables);
@ -75,23 +94,92 @@ public class MetaDataAction
Map<String, QFrontendProcessMetaData> processes = new LinkedHashMap<>();
for(Map.Entry<String, QProcessMetaData> entry : metaDataInput.getInstance().getProcesses().entrySet())
{
processes.put(entry.getKey(), new QFrontendProcessMetaData(entry.getValue(), false));
treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
String processName = entry.getKey();
QProcessMetaData process = entry.getValue();
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, process);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
processes.put(processName, new QFrontendProcessMetaData(metaDataInput, process, false));
treeNodes.put(processName, new AppTreeNode(process));
}
metaDataOutput.setProcesses(processes);
//////////////////////////////////////
// map reports to frontend metadata //
//////////////////////////////////////
Map<String, QFrontendReportMetaData> reports = new LinkedHashMap<>();
for(Map.Entry<String, QReportMetaData> entry : metaDataInput.getInstance().getReports().entrySet())
{
String reportName = entry.getKey();
QReportMetaData report = entry.getValue();
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, report);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
reports.put(reportName, new QFrontendReportMetaData(metaDataInput, report, false));
treeNodes.put(reportName, new AppTreeNode(report));
}
metaDataOutput.setReports(reports);
//////////////////////////////////////
// map widgets to frontend metadata //
//////////////////////////////////////
Map<String, QFrontendWidgetMetaData> widgets = new LinkedHashMap<>();
for(Map.Entry<String, QWidgetMetaDataInterface> entry : metaDataInput.getInstance().getWidgets().entrySet())
{
String widgetName = entry.getKey();
QWidgetMetaDataInterface widget = entry.getValue();
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, widget);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
widgets.put(widgetName, new QFrontendWidgetMetaData(metaDataInput, widget));
}
metaDataOutput.setWidgets(widgets);
///////////////////////////////////
// map apps to frontend metadata //
///////////////////////////////////
Map<String, QFrontendAppMetaData> apps = new LinkedHashMap<>();
for(Map.Entry<String, QAppMetaData> entry : metaDataInput.getInstance().getApps().entrySet())
{
apps.put(entry.getKey(), new QFrontendAppMetaData(entry.getValue()));
treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
String appName = entry.getKey();
QAppMetaData app = entry.getValue();
for(QAppChildMetaData child : entry.getValue().getChildren())
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, app);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
apps.get(entry.getKey()).addChild(new AppTreeNode(child));
continue;
}
apps.put(appName, new QFrontendAppMetaData(app, metaDataOutput));
treeNodes.put(appName, new AppTreeNode(app));
if(CollectionUtils.nullSafeHasContents(app.getChildren()))
{
for(QAppChildMetaData child : app.getChildren())
{
if(child instanceof MetaDataWithPermissionRules metaDataWithPermissionRules)
{
PermissionCheckResult childPermissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, metaDataWithPermissionRules);
if(childPermissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
}
apps.get(appName).addChild(new AppTreeNode(child));
}
}
}
metaDataOutput.setApps(apps);
@ -104,11 +192,21 @@ public class MetaDataAction
{
if(appMetaData.getParentAppName() == null)
{
buildAppTree(treeNodes, appTree, appMetaData);
buildAppTree(metaDataInput, treeNodes, appTree, appMetaData);
}
}
metaDataOutput.setAppTree(appTree);
////////////////////////////////////
// add branding metadata if found //
////////////////////////////////////
if(metaDataInput.getInstance().getBranding() != null)
{
metaDataOutput.setBranding(metaDataInput.getInstance().getBranding());
}
metaDataOutput.setEnvironmentValues(metaDataInput.getInstance().getEnvironmentValues());
// todo post-customization - can do whatever w/ the result if you want?
return metaDataOutput;
@ -119,7 +217,7 @@ public class MetaDataAction
/*******************************************************************************
**
*******************************************************************************/
private void buildAppTree(Map<String, AppTreeNode> treeNodes, List<AppTreeNode> nodeList, QAppChildMetaData childMetaData)
private void buildAppTree(MetaDataInput metaDataInput, Map<String, AppTreeNode> treeNodes, List<AppTreeNode> nodeList, QAppChildMetaData childMetaData)
{
AppTreeNode treeNode = treeNodes.get(childMetaData.getName());
if(treeNode == null)
@ -134,7 +232,16 @@ public class MetaDataAction
{
for(QAppChildMetaData child : app.getChildren())
{
buildAppTree(treeNodes, treeNode.getChildren(), child);
if(child instanceof MetaDataWithPermissionRules metaDataWithPermissionRules)
{
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, metaDataWithPermissionRules);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
}
buildAppTree(metaDataInput, treeNodes, treeNode.getChildren(), child);
}
}
}

View File

@ -52,7 +52,7 @@ public class ProcessMetaDataAction
{
throw (new QNotFoundException("Process [" + processMetaDataInput.getProcessName() + "] was not found."));
}
processMetaDataOutput.setProcess(new QFrontendProcessMetaData(process, true));
processMetaDataOutput.setProcess(new QFrontendProcessMetaData(processMetaDataInput, process, true));
// todo post-customization - can do whatever w/ the result if you want

View File

@ -27,6 +27,7 @@ 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;
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -52,7 +53,8 @@ public class TableMetaDataAction
{
throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found."));
}
tableMetaDataOutput.setTable(new QFrontendTableMetaData(table, true));
QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName());
tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true));
// todo post-customization - can do whatever w/ the result if you want

View File

@ -0,0 +1,204 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/*******************************************************************************
**
*******************************************************************************/
public class AvailablePermission extends QRecordEntity
{
public static final String TABLE_NAME = "availablePermission";
@QField(label = "Permission Name")
private String name;
@QField(label = "Object")
private String objectName;
@QField()
private String objectType;
@QField()
private String permissionType;
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean equals(Object o)
{
if(this == o)
{
return true;
}
if(o == null || getClass() != o.getClass())
{
return false;
}
AvailablePermission that = (AvailablePermission) o;
return Objects.equals(name, that.name) && Objects.equals(objectName, that.objectName) && Objects.equals(objectType, that.objectType) && Objects.equals(permissionType, that.permissionType);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public int hashCode()
{
return Objects.hash(name, objectName, objectType, permissionType);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public AvailablePermission withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for objectType
*******************************************************************************/
public String getObjectType()
{
return (this.objectType);
}
/*******************************************************************************
** Setter for objectType
*******************************************************************************/
public void setObjectType(String objectType)
{
this.objectType = objectType;
}
/*******************************************************************************
** Fluent setter for objectType
*******************************************************************************/
public AvailablePermission withObjectType(String objectType)
{
this.objectType = objectType;
return (this);
}
/*******************************************************************************
** Getter for permissionType
*******************************************************************************/
public String getPermissionType()
{
return (this.permissionType);
}
/*******************************************************************************
** Setter for permissionType
*******************************************************************************/
public void setPermissionType(String permissionType)
{
this.permissionType = permissionType;
}
/*******************************************************************************
** Fluent setter for permissionType
*******************************************************************************/
public AvailablePermission withPermissionType(String permissionType)
{
this.permissionType = permissionType;
return (this);
}
/*******************************************************************************
** Getter for objectName
*******************************************************************************/
public String getObjectName()
{
return (this.objectName);
}
/*******************************************************************************
** Setter for objectName
*******************************************************************************/
public void setObjectName(String objectName)
{
this.objectName = objectName;
}
/*******************************************************************************
** Fluent setter for objectName
*******************************************************************************/
public AvailablePermission withObjectName(String objectName)
{
this.objectName = objectName;
return (this);
}
}

View File

@ -0,0 +1,67 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
/*******************************************************************************
**
*******************************************************************************/
public class BulkTableActionProcessPermissionChecker implements CustomPermissionChecker
{
private static final QLogger LOG = QLogger.getLogger(BulkTableActionProcessPermissionChecker.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException
{
String processName = metaDataWithPermissionRules.getName();
if(processName != null && processName.indexOf('.') > -1)
{
String[] parts = processName.split("\\.", 2);
String tableName = parts[0];
String bulkActionName = parts[1];
AbstractTableActionInput tableActionInput = new AbstractTableActionInput();
tableActionInput.setTableName(tableName);
switch(bulkActionName)
{
case "bulkInsert" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.INSERT);
case "bulkEdit" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.EDIT);
case "bulkDelete" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.DELETE);
default -> LOG.warn("Unexpected bulk action name when checking permissions for process: " + processName);
}
}
}
}

View File

@ -0,0 +1,41 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
/*******************************************************************************
**
*******************************************************************************/
public interface CustomPermissionChecker
{
/*******************************************************************************
**
*******************************************************************************/
void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException;
}

View File

@ -0,0 +1,33 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
/*******************************************************************************
**
*******************************************************************************/
public enum PermissionCheckResult
{
ALLOW,
DENY_HIDE,
DENY_DISABLE;
}

View File

@ -0,0 +1,36 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
/*******************************************************************************
**
*******************************************************************************/
sealed interface PermissionSubType permits PrivatePermissionSubType, TablePermissionSubType
{
/*******************************************************************************
**
*******************************************************************************/
String getPermissionSuffix();
}

View File

@ -0,0 +1,588 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithName;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
**
*******************************************************************************/
public class PermissionsHelper
{
private static final QLogger LOG = QLogger.getLogger(PermissionsHelper.class);
/*******************************************************************************
**
*******************************************************************************/
public static void checkTablePermissionThrowing(AbstractTableActionInput tableActionInput, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
{
checkTablePermissionThrowing(tableActionInput, tableActionInput.getTableName(), permissionSubType);
}
/*******************************************************************************
**
*******************************************************************************/
private static void checkTablePermissionThrowing(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
{
warnAboutPermissionSubTypeForTables(permissionSubType);
QTableMetaData table = actionInput.getInstance().getTable(tableName);
commonCheckPermissionThrowing(getEffectivePermissionRules(table, actionInput.getInstance()), permissionSubType, table.getName(), actionInput);
}
/*******************************************************************************
**
*******************************************************************************/
public static boolean hasTablePermission(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType)
{
try
{
checkTablePermissionThrowing(actionInput, tableName, permissionSubType);
return (true);
}
catch(QPermissionDeniedException e)
{
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static PermissionCheckResult getPermissionCheckResult(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules)
{
QPermissionRules rules = getEffectivePermissionRules(metaDataWithPermissionRules, actionInput.getInstance());
String permissionBaseName = getEffectivePermissionBaseName(rules, metaDataWithPermissionRules.getName());
switch(rules.getLevel())
{
case NOT_PROTECTED:
{
/////////////////////////////////////////////////
// if the entity isn't protected, always ALLOW //
/////////////////////////////////////////////////
return PermissionCheckResult.ALLOW;
}
case HAS_ACCESS_PERMISSION:
{
////////////////////////////////////////////////////////////////////////
// if the entity just has a 'has access', then check for 'has access' //
////////////////////////////////////////////////////////////////////////
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.HAS_ACCESS);
}
case READ_WRITE_PERMISSIONS:
{
////////////////////////////////////////////////////////////////
// if the table is configured w/ read/write, check for either //
////////////////////////////////////////////////////////////////
if(metaDataWithPermissionRules instanceof QTableMetaData)
{
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.READ, PrivatePermissionSubType.WRITE);
}
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.HAS_ACCESS);
}
case READ_INSERT_EDIT_DELETE_PERMISSIONS:
{
//////////////////////////////////////////////////////////////////////////
// if the table is configured w/ read/insert/edit/delete, check for any //
//////////////////////////////////////////////////////////////////////////
if(metaDataWithPermissionRules instanceof QTableMetaData)
{
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, TablePermissionSubType.READ, TablePermissionSubType.INSERT, TablePermissionSubType.EDIT, TablePermissionSubType.DELETE);
}
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.HAS_ACCESS);
}
default:
{
return getPermissionDeniedCheckResult(rules);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName) throws QPermissionDeniedException
{
checkProcessPermissionThrowing(actionInput, processName, Collections.emptyMap());
}
/*******************************************************************************
**
*******************************************************************************/
public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName, Map<String, Serializable> processValues) throws QPermissionDeniedException
{
QProcessMetaData process = actionInput.getInstance().getProcess(processName);
QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, actionInput.getInstance());
if(effectivePermissionRules.getCustomPermissionChecker() != null)
{
/////////////////////////////////////
// todo - avoid stack overflows... //
/////////////////////////////////////
CustomPermissionChecker customPermissionChecker = QCodeLoader.getAdHoc(CustomPermissionChecker.class, effectivePermissionRules.getCustomPermissionChecker());
customPermissionChecker.checkPermissionsThrowing(actionInput, process);
return;
}
commonCheckPermissionThrowing(effectivePermissionRules, PrivatePermissionSubType.HAS_ACCESS, process.getName(), actionInput);
}
/*******************************************************************************
**
*******************************************************************************/
public static boolean hasProcessPermission(AbstractActionInput actionInput, String processName)
{
try
{
checkProcessPermissionThrowing(actionInput, processName);
return (true);
}
catch(QPermissionDeniedException e)
{
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void checkAppPermissionThrowing(AbstractActionInput actionInput, String appName) throws QPermissionDeniedException
{
QAppMetaData app = actionInput.getInstance().getApp(appName);
commonCheckPermissionThrowing(getEffectivePermissionRules(app, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName(), actionInput);
}
/*******************************************************************************
**
*******************************************************************************/
public static boolean hasAppPermission(AbstractActionInput actionInput, String appName)
{
try
{
checkAppPermissionThrowing(actionInput, appName);
return (true);
}
catch(QPermissionDeniedException e)
{
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void checkReportPermissionThrowing(AbstractActionInput actionInput, String reportName) throws QPermissionDeniedException
{
QReportMetaData report = actionInput.getInstance().getReport(reportName);
commonCheckPermissionThrowing(getEffectivePermissionRules(report, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName(), actionInput);
}
/*******************************************************************************
**
*******************************************************************************/
public static boolean hasReportPermission(AbstractActionInput actionInput, String reportName)
{
try
{
checkReportPermissionThrowing(actionInput, reportName);
return (true);
}
catch(QPermissionDeniedException e)
{
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void checkWidgetPermissionThrowing(AbstractActionInput actionInput, String widgetName) throws QPermissionDeniedException
{
QWidgetMetaDataInterface widget = actionInput.getInstance().getWidget(widgetName);
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName(), actionInput);
}
/*******************************************************************************
**
*******************************************************************************/
public static boolean hasWidgetPermission(AbstractActionInput actionInput, String widgetName)
{
try
{
checkWidgetPermissionThrowing(actionInput, widgetName);
return (true);
}
catch(QPermissionDeniedException e)
{
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static Collection<String> getAllAvailablePermissionNames(QInstance instance)
{
return (getAllAvailablePermissions(instance).stream()
.map(AvailablePermission::getName)
.collect(Collectors.toCollection(LinkedHashSet::new)));
}
/*******************************************************************************
**
*******************************************************************************/
public static Collection<AvailablePermission> getAllAvailablePermissions(QInstance instance)
{
Collection<AvailablePermission> rs = new LinkedHashSet<>();
for(QTableMetaData tableMetaData : instance.getTables().values())
{
if(tableMetaData.getIsHidden())
{
continue;
}
QPermissionRules rules = getEffectivePermissionRules(tableMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, tableMetaData.getName());
for(TablePermissionSubType permissionSubType : TablePermissionSubType.values())
{
addEffectiveAvailablePermission(rules, permissionSubType, rs, baseName, tableMetaData, "Table");
}
}
for(QProcessMetaData processMetaData : instance.getProcesses().values())
{
if(processMetaData.getIsHidden())
{
continue;
}
QPermissionRules rules = getEffectivePermissionRules(processMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, processMetaData.getName());
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, processMetaData, "Process");
}
for(QAppMetaData appMetaData : instance.getApps().values())
{
QPermissionRules rules = getEffectivePermissionRules(appMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, appMetaData.getName());
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, appMetaData, "App");
}
for(QReportMetaData reportMetaData : instance.getReports().values())
{
QPermissionRules rules = getEffectivePermissionRules(reportMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, reportMetaData.getName());
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, reportMetaData, "Report");
}
for(QWidgetMetaDataInterface widgetMetaData : instance.getWidgets().values())
{
QPermissionRules rules = getEffectivePermissionRules(widgetMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, widgetMetaData.getName());
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, widgetMetaData, "Widget");
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private static void addEffectiveAvailablePermission(QPermissionRules rules, PermissionSubType permissionSubType, Collection<AvailablePermission> rs, String baseName, MetaDataWithName metaDataWithName, String objectType)
{
PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType);
if(effectivePermissionSubType != null)
{
rs.add(new AvailablePermission()
.withName(getPermissionName(baseName, effectivePermissionSubType))
.withObjectName(metaDataWithName.getLabel())
.withPermissionType(effectivePermissionSubType.toString())
.withObjectType(objectType));
}
}
/*******************************************************************************
**
*******************************************************************************/
public static QPermissionRules getEffectivePermissionRules(MetaDataWithPermissionRules metaDataWithPermissionRules, QInstance instance)
{
if(metaDataWithPermissionRules.getPermissionRules() == null)
{
LOG.warn("Null permission rules on meta data object [" + metaDataWithPermissionRules.getClass().getSimpleName() + "][" + metaDataWithPermissionRules.getName() + "] - does the instance need enriched? Returning instance default rules.");
return (instance.getDefaultPermissionRules());
}
return (metaDataWithPermissionRules.getPermissionRules());
}
/*******************************************************************************
**
*******************************************************************************/
static boolean hasPermission(QSession session, String permissionBaseName, PermissionSubType permissionSubType)
{
if(permissionSubType == null)
{
return (true);
}
String permissionName = getPermissionName(permissionBaseName, permissionSubType);
return (session.hasPermission(permissionName));
}
/*******************************************************************************
**
*******************************************************************************/
static PermissionCheckResult getPermissionCheckResult(AbstractActionInput actionInput, QPermissionRules rules, String permissionBaseName, MetaDataWithPermissionRules metaDataWithPermissionRules, PermissionSubType... permissionSubTypes)
{
for(PermissionSubType permissionSubType : permissionSubTypes)
{
PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType);
if(rules.getCustomPermissionChecker() != null)
{
try
{
CustomPermissionChecker customPermissionChecker = QCodeLoader.getAdHoc(CustomPermissionChecker.class, rules.getCustomPermissionChecker());
customPermissionChecker.checkPermissionsThrowing(actionInput, metaDataWithPermissionRules);
return (PermissionCheckResult.ALLOW);
}
catch(QPermissionDeniedException e)
{
return (getPermissionDeniedCheckResult(rules));
}
}
if(hasPermission(actionInput.getSession(), permissionBaseName, effectivePermissionSubType))
{
return (PermissionCheckResult.ALLOW);
}
}
return (getPermissionDeniedCheckResult(rules));
}
/*******************************************************************************
**
*******************************************************************************/
static String getEffectivePermissionBaseName(QPermissionRules rules, String standardName)
{
if(rules != null && StringUtils.hasContent(rules.getPermissionBaseName()))
{
return (rules.getPermissionBaseName());
}
return (standardName);
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
static PermissionSubType getEffectivePermissionSubType(QPermissionRules rules, PermissionSubType originalPermissionSubType)
{
if(rules == null || rules.getLevel() == null)
{
return (originalPermissionSubType);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the original permission sub-type is "hasAccess" - then this is a check for a process/report/widget. //
// in that case - never return the table-level read/write/insert/edit/delete options //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(PrivatePermissionSubType.HAS_ACCESS.equals(originalPermissionSubType))
{
return switch(rules.getLevel())
{
case NOT_PROTECTED -> null;
default -> PrivatePermissionSubType.HAS_ACCESS;
};
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, this is a table check - so - based on the rules being used for this table, map the requested //
// permission sub-type to what we expect to be set for the table //
////////////////////////////////////////////////////////////////////////////////////////////////////////
return switch(rules.getLevel())
{
case NOT_PROTECTED -> null;
case HAS_ACCESS_PERMISSION -> PrivatePermissionSubType.HAS_ACCESS;
case READ_WRITE_PERMISSIONS ->
{
if(PrivatePermissionSubType.READ.equals(originalPermissionSubType) || PrivatePermissionSubType.WRITE.equals(originalPermissionSubType))
{
yield (originalPermissionSubType);
}
else if(TablePermissionSubType.INSERT.equals(originalPermissionSubType) || TablePermissionSubType.EDIT.equals(originalPermissionSubType) || TablePermissionSubType.DELETE.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.WRITE);
}
else if(TablePermissionSubType.READ.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.READ);
}
else
{
throw new IllegalStateException("Unexpected permissionSubType: " + originalPermissionSubType);
}
}
case READ_INSERT_EDIT_DELETE_PERMISSIONS -> originalPermissionSubType;
};
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void commonCheckPermissionThrowing(QPermissionRules rules, PermissionSubType permissionSubType, String name, AbstractActionInput actionInput) throws QPermissionDeniedException
{
PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType);
String permissionBaseName = getEffectivePermissionBaseName(rules, name);
if(effectivePermissionSubType == null)
{
return;
}
if(!hasPermission(actionInput.getSession(), permissionBaseName, effectivePermissionSubType))
{
// LOG.debug("Throwing permission denied for: " + getPermissionName(permissionBaseName, effectivePermissionSubType) + " for " + actionInput.getSession().getUser());
throw (new QPermissionDeniedException("Permission denied."));
}
}
/*******************************************************************************
**
*******************************************************************************/
private static String getPermissionName(String permissionBaseName, PermissionSubType permissionSubType)
{
return permissionBaseName + "." + permissionSubType.getPermissionSuffix();
}
/*******************************************************************************
**
*******************************************************************************/
private static PermissionCheckResult getPermissionDeniedCheckResult(QPermissionRules rules)
{
if(rules == null || rules.getDenyBehavior() == null || rules.getDenyBehavior().equals(DenyBehavior.HIDDEN))
{
return (PermissionCheckResult.DENY_HIDE);
}
else
{
return (PermissionCheckResult.DENY_DISABLE);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void warnAboutPermissionSubTypeForTables(PermissionSubType permissionSubType)
{
if(permissionSubType == null)
{
return;
}
if(permissionSubType == PrivatePermissionSubType.HAS_ACCESS)
{
LOG.warn("PermissionSubType.HAS_ACCESS should not be checked for a table");
}
}
}

View File

@ -0,0 +1,56 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
/*******************************************************************************
**
*******************************************************************************/
enum PrivatePermissionSubType implements PermissionSubType
{
HAS_ACCESS("hasAccess"), // for processes, reports, etc - basically, not tables.
READ("read"), // for a table in read/write mode - or - for read (query, get, count) on a table in full-mode
WRITE("write");
private final String permissionSuffix;
/*******************************************************************************
**
*******************************************************************************/
PrivatePermissionSubType(String permissionSuffix)
{
this.permissionSuffix = permissionSuffix;
}
/*******************************************************************************
** Getter for permissionSuffix
*******************************************************************************/
public String getPermissionSuffix()
{
return (this.permissionSuffix);
}
}

View File

@ -0,0 +1,53 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
/*******************************************************************************
**
*******************************************************************************/
public class ReportProcessPermissionChecker implements CustomPermissionChecker
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException
{
if(actionInput instanceof RunProcessInput runProcessInput)
{
String reportName = runProcessInput.getValueString("reportName");
if(reportName != null)
{
PermissionsHelper.checkReportPermissionThrowing(actionInput, reportName);
}
}
}
}

View File

@ -0,0 +1,57 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
/*******************************************************************************
**
*******************************************************************************/
public enum TablePermissionSubType implements PermissionSubType
{
READ("read"), // for a table in read/write mode - or - for read (query, get, count) on a table in full-mode
INSERT("insert"), // for table-insert.
EDIT("edit"), // for table-edit.
DELETE("delete"); // for table-delete.
private final String permissionSuffix;
/*******************************************************************************
**
*******************************************************************************/
TablePermissionSubType(String permissionSuffix)
{
this.permissionSuffix = permissionSuffix;
}
/*******************************************************************************
** Getter for permissionSuffix
*******************************************************************************/
public String getPermissionSuffix()
{
return (this.permissionSuffix);
}
}

View File

@ -39,10 +39,16 @@ public interface QProcessCallback
/*******************************************************************************
** Get the filter query for this callback.
*******************************************************************************/
QQueryFilter getQueryFilter();
default QQueryFilter getQueryFilter()
{
return (null);
}
/*******************************************************************************
** Get the field values for this callback.
*******************************************************************************/
Map<String, Serializable> getFieldValues(List<QFieldMetaData> fields);
default Map<String, Serializable> getFieldValues(List<QFieldMetaData> fields)
{
return (null);
}
}

View File

@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
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.QueryInput;
@ -42,8 +43,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMet
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.utils.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -52,7 +51,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class RunBackendStepAction
{
private static final Logger LOG = LogManager.getLogger(RunBackendStepAction.class);
private static final QLogger LOG = QLogger.getLogger(RunBackendStepAction.class);
@ -175,8 +174,7 @@ public class RunBackendStepAction
{
if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
{
QueryInput queryInput = new QueryInput(runBackendStepInput.getInstance());
queryInput.setSession(runBackendStepInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(inputMetaData.getRecordListMetaData().getTableName());
// todo - handle this being async (e.g., http)

View File

@ -23,28 +23,45 @@ package com.kingsrook.qqq.backend.core.actions.processes;
import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
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.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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.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.processes.QBackendStepMetaData;
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.QStepMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
/*******************************************************************************
@ -53,7 +70,17 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class RunProcessAction
{
private static final Logger LOG = LogManager.getLogger(RunProcessAction.class);
private static final QLogger LOG = QLogger.getLogger(RunProcessAction.class);
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";
////////////////////////////////////////////////////////////////////////////////////////////////
// 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";
@ -82,16 +109,41 @@ public class RunProcessAction
runProcessOutput.setProcessUUID(runProcessInput.getProcessUUID());
UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS);
ProcessState processState = primeProcessState(runProcessInput, stateKey);
ProcessState processState = primeProcessState(runProcessInput, stateKey, process);
/////////////////////////////////////////////////////////
// if process is 'basepull' style, keep track of 'now' //
/////////////////////////////////////////////////////////
BasepullConfiguration basepullConfiguration = process.getBasepullConfiguration();
if(basepullConfiguration != null)
{
///////////////////////////////////////
// get the stored basepull timestamp //
///////////////////////////////////////
persistLastRunTime(runProcessInput, process, basepullConfiguration);
}
// todo - custom routing
List<QStepMetaData> stepList = getAvailableStepList(process, runProcessInput);
try
{
String lastStepName = runProcessInput.getStartAfterStep();
STEP_LOOP:
for(QStepMetaData step : stepList)
while(true)
{
if(step instanceof QFrontendStepMetaData)
///////////////////////////////////////////////////////////////////////////////////////////////////////
// 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 //
@ -100,13 +152,14 @@ public class RunProcessAction
{
case BREAK ->
{
LOG.info("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
processFrontendStepFieldDefaultValues(processState, frontendStep);
processState.setNextStepName(step.getName());
break STEP_LOOP;
}
case SKIP ->
{
LOG.info("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
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... //
@ -116,7 +169,7 @@ public class RunProcessAction
}
case FAIL ->
{
LOG.info("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
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());
@ -127,6 +180,7 @@ public class RunProcessAction
///////////////////////
// Run backend steps //
///////////////////////
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
}
else
@ -137,6 +191,18 @@ public class RunProcessAction
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
}
}
///////////////////////////////////////////////////////////////////////////
// if 'basepull' style process, update the stored basepull timestamp //
// but only when we've been signaled to do so - i.e., only if we did our //
// query using the timestamp field, and only after an Execute step runs. //
///////////////////////////////////////////////////////////////////////////
if(basepullConfiguration != null
&& BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(runProcessInput.getValue(BASEPULL_DID_QUERY_USING_TIMESTAMP_FIELD)))
&& BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(runProcessInput.getValue(BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD))))
{
storeLastRunTime(runProcessInput, process, basepullConfiguration);
}
}
catch(QException qe)
{
@ -165,11 +231,27 @@ public class RunProcessAction
/*******************************************************************************
**
*******************************************************************************/
private void processFrontendStepFieldDefaultValues(ProcessState processState, QFrontendStepMetaData step)
{
for(QFieldMetaData formField : CollectionUtils.mergeLists(step.getFormFields(), step.getInputFields(), step.getViewFields(), step.getOutputFields()))
{
if(formField.getDefaultValue() != null && processState.getValues().get(formField.getName()) == null)
{
processState.getValues().put(formField.getName(), formField.getDefaultValue());
}
}
}
/*******************************************************************************
** When we start running a process (or resuming it), get data in the RunProcessRequest
** either from the state provider (if they're found, for a resume).
*******************************************************************************/
ProcessState primeProcessState(RunProcessInput runProcessInput, UUIDAndTypeStateKey stateKey) throws QException
ProcessState primeProcessState(RunProcessInput runProcessInput, UUIDAndTypeStateKey stateKey, QProcessMetaData process) throws QException
{
Optional<ProcessState> optionalProcessState = loadState(stateKey);
if(optionalProcessState.isEmpty())
@ -177,11 +259,14 @@ public class RunProcessAction
if(runProcessInput.getStartAfterStep() == null)
{
///////////////////////////////////////////////////////////////////////////////////
// this is fine - it means its our first time running in the backend. //
// This condition (no state in state-provider, and no start-after-step) means //
// that we're starting a new process! Init the process state here, then //
// Go ahead and store the state that we have (e.g., w/ initial records & values) //
///////////////////////////////////////////////////////////////////////////////////
storeState(stateKey, runProcessInput.getProcessState());
optionalProcessState = Optional.of(runProcessInput.getProcessState());
ProcessState processState = runProcessInput.getProcessState();
processState.setStepList(process.getStepList().stream().map(QStepMetaData::getName).toList());
storeState(stateKey, processState);
optionalProcessState = Optional.of(processState);
}
else
{
@ -227,13 +312,22 @@ public class RunProcessAction
*******************************************************************************/
private void runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
{
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(runProcessInput.getInstance(), processState);
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
runBackendStepInput.setProcessName(process.getName());
runBackendStepInput.setStepName(backendStep.getName());
runBackendStepInput.setTableName(process.getTableName());
runBackendStepInput.setSession(runProcessInput.getSession());
runBackendStepInput.setCallback(runProcessInput.getCallback());
runBackendStepInput.setFrontendStepBehavior(runProcessInput.getFrontendStepBehavior());
runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback());
///////////////////////////////////////////////////////////////
// if 'basepull' values are in the inputs, add to step input //
///////////////////////////////////////////////////////////////
if(runProcessInput.getValues().containsKey(BASEPULL_LAST_RUNTIME_KEY))
{
runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY));
}
RunBackendStepOutput lastFunctionResult = new RunBackendStepAction().execute(runBackendStepInput);
storeState(stateKey, lastFunctionResult.getProcessState());
@ -249,41 +343,63 @@ public class RunProcessAction
/*******************************************************************************
** Get the list of steps which are eligible to run.
*******************************************************************************/
private List<QStepMetaData> getAvailableStepList(QProcessMetaData process, RunProcessInput runProcessInput)
private List<QStepMetaData> getAvailableStepList(ProcessState processState, QProcessMetaData process, String lastStep) throws QException
{
if(runProcessInput.getStartAfterStep() == null)
if(lastStep == null)
{
/////////////////////////////////////////////////////////////////////////////
// if the caller did not supply a 'startAfterStep', then use the full list //
/////////////////////////////////////////////////////////////////////////////
return (process.getStepList());
///////////////////////////////////////////////////////////////////////
// if the caller did not supply a 'lastStep', then use the full list //
///////////////////////////////////////////////////////////////////////
return (stepNamesToSteps(process, processState.getStepList()));
}
else
{
////////////////////////////////////////////////////////////////////////////////
// else, loop until the startAfterStep is found, and return the ones after it //
////////////////////////////////////////////////////////////////////////////////
boolean foundStartAfterStep = false;
List<QStepMetaData> rs = new ArrayList<>();
////////////////////////////////////////////////////////////////////////////
// else, loop until the 'lastStep' is found, and return the ones after it //
////////////////////////////////////////////////////////////////////////////
boolean foundLastStep = false;
List<String> validStepNames = new ArrayList<>();
for(QStepMetaData step : process.getStepList())
for(String stepName : processState.getStepList())
{
if(foundStartAfterStep)
if(foundLastStep)
{
rs.add(step);
validStepNames.add(stepName);
}
if(step.getName().equals(runProcessInput.getStartAfterStep()))
if(stepName.equals(lastStep))
{
foundStartAfterStep = true;
foundLastStep = true;
}
}
return (rs);
return (stepNamesToSteps(process, validStepNames));
}
}
/*******************************************************************************
**
*******************************************************************************/
private List<QStepMetaData> stepNamesToSteps(QProcessMetaData process, List<String> stepNames) throws QException
{
List<QStepMetaData> result = new ArrayList<>();
for(String stepName : stepNames)
{
QStepMetaData step = process.getStep(stepName);
if(step == null)
{
throw (new QException("Could not find a step named [" + stepName + "] in this process."));
}
result.add(step);
}
return (result);
}
/*******************************************************************************
** Load an instance of the appropriate state provider
**
@ -329,4 +445,125 @@ public class RunProcessAction
return (getStateProvider().get(ProcessState.class, stateKey));
}
/*******************************************************************************
** Insert or update the last runtime value for this basepull into the backend.
*******************************************************************************/
protected void storeLastRunTime(RunProcessInput runProcessInput, QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
{
String basepullTableName = basepullConfiguration.getTableName();
String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
///////////////////////////////////////
// get the stored basepull timestamp //
///////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(basepullTableName);
queryInput.setFilter(new QQueryFilter().withCriteria(
new QFilterCriteria()
.withFieldName(basepullKeyFieldName)
.withOperator(QCriteriaOperator.EQUALS)
.withValues(List.of(basepullKeyValue))));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
//////////////////////////////////////////
// get the runtime for this process run //
//////////////////////////////////////////
Instant newRunTime = (Instant) runProcessInput.getValues().get(BASEPULL_THIS_RUNTIME_KEY);
/////////////////////////////////////////////////
// update if found, otherwise insert new value //
/////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
{
///////////////////////////////////////////////////////////////////////////////
// update the basepull table with 'now' (which is before original query ran) //
///////////////////////////////////////////////////////////////////////////////
QRecord basepullRecord = queryOutput.getRecords().get(0);
basepullRecord.setValue(basepullLastRunTimeFieldName, newRunTime);
////////////
// update //
////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(basepullTableName);
updateInput.setRecords(List.of(basepullRecord));
new UpdateAction().execute(updateInput);
}
else
{
QRecord basepullRecord = new QRecord()
.withValue(basepullKeyFieldName, basepullKeyValue)
.withValue(basepullLastRunTimeFieldName, newRunTime);
////////////////////////////////
// insert new basepull record //
////////////////////////////////
InsertInput insertInput = new InsertInput();
insertInput.setTableName(basepullTableName);
insertInput.setRecords(List.of(basepullRecord));
new InsertAction().execute(insertInput);
}
}
/*******************************************************************************
** Lookup the last runtime for this basepull, and set it (plus now) in the process's
** values.
*******************************************************************************/
protected void persistLastRunTime(RunProcessInput runProcessInput, QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
{
////////////////////////////////////////////////////////
// if these values were already computed, don't re-do //
////////////////////////////////////////////////////////
if(runProcessInput.getValue(BASEPULL_THIS_RUNTIME_KEY) != null)
{
return;
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// store 'now', which will be used to update basepull record if process completes successfully //
/////////////////////////////////////////////////////////////////////////////////////////////////
Instant now = Instant.now();
runProcessInput.getValues().put(BASEPULL_THIS_RUNTIME_KEY, now);
String basepullTableName = basepullConfiguration.getTableName();
String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp();
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
///////////////////////////////////////
// get the stored basepull timestamp //
///////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(basepullTableName);
queryInput.setFilter(new QQueryFilter().withCriteria(
new QFilterCriteria()
.withFieldName(basepullKeyFieldName)
.withOperator(QCriteriaOperator.EQUALS)
.withValues(List.of(basepullKeyValue))));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
///////////////////////////////////////////////////////////////////////////////////////////////////
// get the stored time, if not, default to 'now' unless a number of hours to offset was provided //
///////////////////////////////////////////////////////////////////////////////////////////////////
Instant lastRunTime = now;
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
{
QRecord basepullRecord = queryOutput.getRecords().get(0);
lastRunTime = ValueUtils.getValueAsInstant(basepullRecord.getValue(basepullLastRunTimeFieldName));
}
else if(basepullHoursBackForInitialTimestamp != null)
{
lastRunTime = lastRunTime.minus(basepullHoursBackForInitialTimestamp, ChronoUnit.HOURS);
}
runProcessInput.getValues().put(BASEPULL_LAST_RUNTIME_KEY, lastRunTime);
runProcessInput.getValues().put(BASEPULL_TIMESTAMP_FIELD, basepullConfiguration.getTimestampField());
}
}

View File

@ -0,0 +1,215 @@
/*
* 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.queues;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import com.amazonaws.services.sqs.model.DeleteMessageBatchRequest;
import com.amazonaws.services.sqs.model.DeleteMessageBatchRequestEntry;
import com.amazonaws.services.sqs.model.Message;
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
import com.amazonaws.services.sqs.model.ReceiveMessageResult;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
** Class to poll an SQS queue, and run process code for each message found.
*******************************************************************************/
public class SQSQueuePoller implements Runnable
{
private static final QLogger LOG = QLogger.getLogger(SQSQueuePoller.class);
///////////////////////////////////////////////
// todo - move these 2 to a "QBaseRunnable"? //
///////////////////////////////////////////////
private QInstance qInstance;
private Supplier<QSession> sessionSupplier;
private SQSQueueProviderMetaData queueProviderMetaData;
private QQueueMetaData queueMetaData;
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run()
{
QContext.init(qInstance, sessionSupplier.get());
String originalThreadName = Thread.currentThread().getName();
Thread.currentThread().setName("SQSPoller>" + queueMetaData.getName());
LOG.debug("Running " + this.getClass().getSimpleName() + "[" + queueMetaData.getName() + "]");
try
{
BasicAWSCredentials credentials = new BasicAWSCredentials(queueProviderMetaData.getAccessKey(), queueProviderMetaData.getSecretKey());
final AmazonSQS sqs = AmazonSQSClientBuilder.standard()
.withRegion(queueProviderMetaData.getRegion())
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
String queueUrl = queueProviderMetaData.getBaseURL();
if(!queueUrl.endsWith("/"))
{
queueUrl += "/";
}
queueUrl += queueMetaData.getQueueName();
while(true)
{
///////////////////////////////
// fetch a batch of messages //
///////////////////////////////
ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest();
receiveMessageRequest.setQueueUrl(queueUrl);
receiveMessageRequest.setMaxNumberOfMessages(10);
receiveMessageRequest.setWaitTimeSeconds(20); // help urge SQS to query multiple servers and find more messages
ReceiveMessageResult receiveMessageResult = sqs.receiveMessage(receiveMessageRequest);
if(receiveMessageResult.getMessages().isEmpty())
{
LOG.debug("0 messages received. Breaking.");
break;
}
LOG.debug(receiveMessageResult.getMessages().size() + " messages received. Processing.");
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// extract data from the messages into list of bodies to pass into process, and list of delete-batch-inputs //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<DeleteMessageBatchRequestEntry> deleteRequestEntries = new ArrayList<>();
ArrayList<String> bodies = new ArrayList<>();
int i = 0;
for(Message message : receiveMessageResult.getMessages())
{
bodies.add(message.getBody());
deleteRequestEntries.add(new DeleteMessageBatchRequestEntry(String.valueOf(i++), message.getReceiptHandle()));
}
/////////////////////////////////////////////////////////////////////////////////////
// run the process, in a try-catch, so even if it fails, our loop keeps going. //
// the messages in a failed process will get re-delivered, to try-again, up to the //
// number of times configured in AWS //
/////////////////////////////////////////////////////////////////////////////////////
try
{
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(queueMetaData.getProcessName());
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
runProcessInput.addValue("bodies", bodies);
RunProcessAction runProcessAction = new RunProcessAction();
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
////////////////////////////////////////////////////////////////////////////////////////////
// if there was an exception returned by the process (e.g., thrown in backend step), then //
// warn and leave the messages for re-processing. //
////////////////////////////////////////////////////////////////////////////////////////////
if(runProcessOutput.getException().isPresent())
{
LOG.warn("Exception returned by process when handling SQS Messages. They will not be deleted from the queue.", runProcessOutput.getException().get());
}
else
{
///////////////////////////////////////////////
// else, if no exception, do a batch delete. //
///////////////////////////////////////////////
sqs.deleteMessageBatch(new DeleteMessageBatchRequest()
.withQueueUrl(queueUrl)
.withEntries(deleteRequestEntries));
}
}
catch(Exception e)
{
LOG.warn("Error receiving SQS Messages.", e);
}
}
}
catch(Exception e)
{
LOG.warn("Error running SQS Queue Poller", e);
}
finally
{
Thread.currentThread().setName(originalThreadName);
QContext.clear();
}
}
/*******************************************************************************
** Setter for queueProviderMetaData
**
*******************************************************************************/
public void setQueueProviderMetaData(SQSQueueProviderMetaData queueProviderMetaData)
{
this.queueProviderMetaData = queueProviderMetaData;
}
/*******************************************************************************
** Setter for queueMetaData
**
*******************************************************************************/
public void setQueueMetaData(QQueueMetaData queueMetaData)
{
this.queueMetaData = queueMetaData;
}
/*******************************************************************************
** Setter for qInstance
**
*******************************************************************************/
public void setQInstance(QInstance qInstance)
{
this.qInstance = qInstance;
}
/*******************************************************************************
** Setter for sessionSupplier
**
*******************************************************************************/
public void setSessionSupplier(Supplier<QSession> sessionSupplier)
{
this.sessionSupplier = sessionSupplier;
}
}

View File

@ -0,0 +1,89 @@
/*
* 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.reporting;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Subclass of RecordPipe, which uses a buffer in the addRecord method, to avoid
** sending single-records at a time through postRecordActions and to consumers.
*******************************************************************************/
public class BufferedRecordPipe extends RecordPipe
{
private List<QRecord> buffer = new ArrayList<>();
private Integer bufferSize = 100;
/*******************************************************************************
** Constructor - uses default buffer size
**
*******************************************************************************/
public BufferedRecordPipe()
{
}
/*******************************************************************************
** Constructor - customize buffer size.
**
*******************************************************************************/
public BufferedRecordPipe(Integer bufferSize)
{
this.bufferSize = bufferSize;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecord(QRecord record)
{
buffer.add(record);
if(buffer.size() >= bufferSize)
{
addRecords(buffer);
buffer.clear();
}
}
/*******************************************************************************
**
*******************************************************************************/
public void finalFlush()
{
if(!buffer.isEmpty())
{
addRecords(buffer);
buffer.clear();
}
}
}

View File

@ -27,24 +27,24 @@ import java.nio.charset.StandardCharsets;
import java.util.List;
import com.kingsrook.qqq.backend.core.adapters.QRecordToCsvAdapter;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** CSV report format implementation
** CSV export format implementation
*******************************************************************************/
public class CsvReportStreamer implements ReportStreamerInterface
public class CsvExportStreamer implements ExportStreamerInterface
{
private static final Logger LOG = LogManager.getLogger(CsvReportStreamer.class);
private static final QLogger LOG = QLogger.getLogger(CsvExportStreamer.class);
private final QRecordToCsvAdapter qRecordToCsvAdapter;
private ReportInput reportInput;
private ExportInput exportInput;
private QTableMetaData table;
private List<QFieldMetaData> fields;
private OutputStream outputStream;
@ -54,7 +54,7 @@ public class CsvReportStreamer implements ReportStreamerInterface
/*******************************************************************************
**
*******************************************************************************/
public CsvReportStreamer()
public CsvExportStreamer()
{
qRecordToCsvAdapter = new QRecordToCsvAdapter();
}
@ -65,14 +65,14 @@ public class CsvReportStreamer implements ReportStreamerInterface
**
*******************************************************************************/
@Override
public void start(ReportInput reportInput, List<QFieldMetaData> fields) throws QReportingException
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
{
this.reportInput = reportInput;
this.exportInput = exportInput;
this.fields = fields;
table = reportInput.getTable();
outputStream = this.reportInput.getReportOutputStream();
table = exportInput.getTable();
outputStream = this.exportInput.getReportOutputStream();
writeReportHeaderRow();
writeTitleAndHeader();
}
@ -80,9 +80,16 @@ public class CsvReportStreamer implements ReportStreamerInterface
/*******************************************************************************
**
*******************************************************************************/
private void writeReportHeaderRow() throws QReportingException
private void writeTitleAndHeader() throws QReportingException
{
try
{
if(StringUtils.hasContent(exportInput.getTitleRow()))
{
outputStream.write((exportInput.getTitleRow() + "\n").getBytes(StandardCharsets.UTF_8));
}
if(exportInput.getIncludeHeaderRow())
{
int col = 0;
for(QFieldMetaData column : fields)
@ -94,6 +101,8 @@ public class CsvReportStreamer implements ReportStreamerInterface
outputStream.write(('"' + column.getLabel() + '"').getBytes(StandardCharsets.UTF_8));
}
outputStream.write('\n');
}
outputStream.flush();
}
catch(Exception e)
@ -108,21 +117,29 @@ public class CsvReportStreamer implements ReportStreamerInterface
**
*******************************************************************************/
@Override
public int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException
public void addRecords(List<QRecord> qRecords) throws QReportingException
{
List<QRecord> qRecords = recordPipe.consumeAvailableRecords();
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
try
{
for(QRecord qRecord : qRecords)
{
writeRecord(qRecord);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void writeRecord(QRecord qRecord) throws QReportingException
{
try
{
String csv = qRecordToCsvAdapter.recordToCsv(table, qRecord, fields);
outputStream.write(csv.getBytes(StandardCharsets.UTF_8));
outputStream.flush(); // todo - less often?
}
return (qRecords.size());
}
catch(Exception e)
{
throw (new QReportingException("Error writing CSV report", e));
@ -131,6 +148,17 @@ public class CsvReportStreamer implements ReportStreamerInterface
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addTotalsRow(QRecord record) throws QReportingException
{
writeRecord(record);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,343 @@
/*
* 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.reporting;
import java.io.OutputStream;
import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.ExcelStylerInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.PlainExcelStyler;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.dhatim.fastexcel.StyleSetter;
import org.dhatim.fastexcel.Workbook;
import org.dhatim.fastexcel.Worksheet;
/*******************************************************************************
** Excel export format implementation
*******************************************************************************/
public class ExcelExportStreamer implements ExportStreamerInterface
{
private static final QLogger LOG = QLogger.getLogger(ExcelExportStreamer.class);
private ExportInput exportInput;
private QTableMetaData table;
private List<QFieldMetaData> fields;
private OutputStream outputStream;
private ExcelStylerInterface excelStylerInterface = new PlainExcelStyler();
private Map<String, String> excelCellFormats;
private Workbook workbook;
private Worksheet worksheet;
private int row = 0;
private int sheetCount = 0;
/*******************************************************************************
**
*******************************************************************************/
public ExcelExportStreamer()
{
}
/*******************************************************************************
** display formats is a map of field name to Excel format strings (e.g., $#,##0.00)
*******************************************************************************/
@Override
public void setDisplayFormats(Map<String, String> displayFormats)
{
this.excelCellFormats = new HashMap<>();
for(Map.Entry<String, String> entry : displayFormats.entrySet())
{
String excelFormat = DisplayFormat.getExcelFormat(entry.getValue());
if(excelFormat != null)
{
excelCellFormats.put(entry.getKey(), excelFormat);
}
}
}
/*******************************************************************************
** Starts a new worksheet in the current workbook. Can be called multiple times.
*******************************************************************************/
@Override
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
{
try
{
this.exportInput = exportInput;
this.fields = fields;
table = exportInput.getTable();
outputStream = this.exportInput.getReportOutputStream();
this.row = 0;
this.sheetCount++;
/////////////////////////////////////////////////////////////////////////////////////////////////////
// if this is the first call in here (e.g., the workbook hasn't been opened yet), then open it now //
/////////////////////////////////////////////////////////////////////////////////////////////////////
if(workbook == null)
{
String appName = "QQQ";
QInstance instance = exportInput.getInstance();
if(instance != null && instance.getBranding() != null && instance.getBranding().getCompanyName() != null)
{
appName = instance.getBranding().getCompanyName();
}
workbook = new Workbook(outputStream, appName, null);
}
/////////////////////////////////////////////////////////////////////////////////////
// if start is called a second time (e.g., and there's already an open worksheet), //
// finish that sheet, before a new one is created. //
/////////////////////////////////////////////////////////////////////////////////////
if(worksheet != null)
{
worksheet.finish();
}
worksheet = workbook.newWorksheet(Objects.requireNonNullElse(label, "Sheet" + sheetCount));
writeTitleAndHeader();
}
catch(Exception e)
{
throw (new QReportingException("Error starting worksheet", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private void writeTitleAndHeader() throws QReportingException
{
try
{
///////////////
// title row //
///////////////
if(StringUtils.hasContent(exportInput.getTitleRow()))
{
worksheet.value(row, 0, exportInput.getTitleRow());
worksheet.range(row, 0, row, fields.size() - 1).merge();
StyleSetter titleStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
excelStylerInterface.styleTitleRow(titleStyle);
titleStyle.set();
row++;
worksheet.flush();
}
////////////////
// header row //
////////////////
if(exportInput.getIncludeHeaderRow())
{
int col = 0;
for(QFieldMetaData column : fields)
{
worksheet.value(row, col, column.getLabel());
col++;
}
StyleSetter headerStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
excelStylerInterface.styleHeaderRow(headerStyle);
headerStyle.set();
row++;
worksheet.flush();
}
}
catch(Exception e)
{
throw (new QReportingException("Error starting Excel report"));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecords(List<QRecord> qRecords) throws QReportingException
{
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
try
{
for(QRecord qRecord : qRecords)
{
writeRecord(qRecord);
row++;
worksheet.flush(); // todo? not at all? or just sometimes?
}
}
catch(Exception e)
{
LOG.error("Exception generating excel file", e);
try
{
workbook.finish();
outputStream.close();
}
finally
{
throw (new QReportingException("Error generating Excel report", e));
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void writeRecord(QRecord qRecord)
{
int col = 0;
for(QFieldMetaData field : fields)
{
Serializable value = qRecord.getValue(field.getName());
if(value != null)
{
if(value instanceof String s)
{
worksheet.value(row, col, s);
}
else if(value instanceof Number n)
{
worksheet.value(row, col, n);
if(excelCellFormats != null)
{
String format = excelCellFormats.get(field.getName());
if(format != null)
{
worksheet.style(row, col).format(format).set();
}
}
}
else if(value instanceof Boolean b)
{
worksheet.value(row, col, b);
}
else if(value instanceof Date d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd").set();
}
else if(value instanceof LocalDate d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd").set();
}
else if(value instanceof LocalDateTime d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
}
else if(value instanceof ZonedDateTime d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
}
else if(value instanceof Instant i)
{
// todo - what would be a better zone to use here?
worksheet.value(row, col, i.atZone(ZoneId.systemDefault()));
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
}
else
{
worksheet.value(row, col, ValueUtils.getValueAsString(value));
}
}
col++;
}
}
/*******************************************************************************
**
*******************************************************************************/
public void addTotalsRow(QRecord record)
{
writeRecord(record);
StyleSetter totalsRowStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
excelStylerInterface.styleTotalsRow(totalsRowStyle);
totalsRowStyle.set();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void finish() throws QReportingException
{
try
{
if(workbook != null)
{
workbook.finish();
}
}
catch(Exception e)
{
throw (new QReportingException("Error finishing Excel report", e));
}
}
}

View File

@ -1,214 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting;
import java.io.OutputStream;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
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;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dhatim.fastexcel.Workbook;
import org.dhatim.fastexcel.Worksheet;
/*******************************************************************************
** Excel report format implementation
*******************************************************************************/
public class ExcelReportStreamer implements ReportStreamerInterface
{
private static final Logger LOG = LogManager.getLogger(ExcelReportStreamer.class);
private ReportInput reportInput;
private QTableMetaData table;
private List<QFieldMetaData> fields;
private OutputStream outputStream;
private Workbook workbook;
private Worksheet worksheet;
private int row = 1;
/*******************************************************************************
**
*******************************************************************************/
public ExcelReportStreamer()
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void start(ReportInput reportInput, List<QFieldMetaData> fields) throws QReportingException
{
this.reportInput = reportInput;
this.fields = fields;
table = reportInput.getTable();
outputStream = this.reportInput.getReportOutputStream();
workbook = new Workbook(outputStream, "QQQ", null);
worksheet = workbook.newWorksheet("Sheet 1");
writeReportHeaderRow();
}
/*******************************************************************************
**
*******************************************************************************/
private void writeReportHeaderRow() throws QReportingException
{
try
{
int col = 0;
for(QFieldMetaData column : fields)
{
worksheet.value(0, col, column.getLabel());
col++;
}
worksheet.flush();
}
catch(Exception e)
{
throw (new QReportingException("Error starting Excel report"));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException
{
List<QRecord> qRecords = recordPipe.consumeAvailableRecords();
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
try
{
for(QRecord qRecord : qRecords)
{
int col = 0;
for(QFieldMetaData column : fields)
{
Serializable value = qRecord.getValue(column.getName());
if(value != null)
{
if(value instanceof String s)
{
worksheet.value(row, col, s);
}
else if(value instanceof Number n)
{
worksheet.value(row, col, n);
}
else if(value instanceof Boolean b)
{
worksheet.value(row, col, b);
}
else if(value instanceof Date d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd").set();
}
else if(value instanceof LocalDate d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd").set();
}
else if(value instanceof LocalDateTime d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
}
else if(value instanceof ZonedDateTime d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
}
else
{
worksheet.value(row, col, ValueUtils.getValueAsString(value));
}
}
col++;
}
row++;
worksheet.flush(); // todo? not at all? or just sometimes?
}
}
catch(Exception e)
{
try
{
workbook.finish();
outputStream.close();
}
finally
{
throw (new QReportingException("Error generating Excel report", e));
}
}
return (qRecords.size());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void finish() throws QReportingException
{
try
{
if(workbook != null)
{
workbook.finish();
}
}
catch(Exception e)
{
throw (new QReportingException("Error finishing Excel report", e));
}
}
}

View File

@ -31,29 +31,30 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
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.ExportOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Action to generate a report.
** Action to generate an export from a table
**
** At this time (future may change?), this action starts a new thread to run
** the query in the backend module. As records are produced by the query,
@ -63,9 +64,9 @@ import org.apache.logging.log4j.Logger;
** time the report outputStream can be closed.
**
*******************************************************************************/
public class ReportAction
public class ExportAction
{
private static final Logger LOG = LogManager.getLogger(ReportAction.class);
private static final QLogger LOG = QLogger.getLogger(ExportAction.class);
private boolean preExecuteRan = false;
private Integer countFromPreExecute = null;
@ -82,21 +83,21 @@ public class ReportAction
** first, in their thread, to catch any validation errors before they start
** the thread (which they may abandon).
*******************************************************************************/
public void preExecute(ReportInput reportInput) throws QException
public void preExecute(ExportInput exportInput) throws QException
{
ActionHelper.validateSession(reportInput);
ActionHelper.validateSession(exportInput);
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(reportInput.getBackend());
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(exportInput.getBackend());
///////////////////////////////////
// verify field names (if given) //
///////////////////////////////////
if(CollectionUtils.nullSafeHasContents(reportInput.getFieldNames()))
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
{
QTableMetaData table = reportInput.getTable();
QTableMetaData table = exportInput.getTable();
List<String> badFieldNames = new ArrayList<>();
for(String fieldName : reportInput.getFieldNames())
for(String fieldName : exportInput.getFieldNames())
{
try
{
@ -119,8 +120,8 @@ public class ReportAction
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// check if this report format has a max-rows limit -- if so, do a count to verify we're under the limit //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
ReportFormat reportFormat = reportInput.getReportFormat();
verifyCountUnderMax(reportInput, backendModule, reportFormat);
ReportFormat reportFormat = exportInput.getReportFormat();
verifyCountUnderMax(exportInput, backendModule, reportFormat);
preExecuteRan = true;
}
@ -130,47 +131,48 @@ public class ReportAction
/*******************************************************************************
** Run the report.
*******************************************************************************/
public ReportOutput execute(ReportInput reportInput) throws QException
public ExportOutput execute(ExportInput exportInput) throws QException
{
if(!preExecuteRan)
{
/////////////////////////////////////
// ensure that pre-execute has ran //
/////////////////////////////////////
preExecute(reportInput);
preExecute(exportInput);
}
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(reportInput.getBackend());
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(exportInput.getBackend());
//////////////////////////
// set up a query input //
//////////////////////////
QueryInterface queryInterface = backendModule.getQueryInterface();
QueryInput queryInput = new QueryInput(reportInput.getInstance());
queryInput.setSession(reportInput.getSession());
queryInput.setTableName(reportInput.getTableName());
queryInput.setFilter(reportInput.getQueryFilter());
queryInput.setLimit(reportInput.getLimit());
QueryAction queryAction = new QueryAction();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(exportInput.getTableName());
queryInput.setFilter(exportInput.getQueryFilter());
queryInput.setLimit(exportInput.getLimit());
queryInput.setShouldTranslatePossibleValues(true);
/////////////////////////////////////////////////////////////////
// tell this query that it needs to put its output into a pipe //
/////////////////////////////////////////////////////////////////
RecordPipe recordPipe = new RecordPipe();
RecordPipe recordPipe = new BufferedRecordPipe(500);
queryInput.setRecordPipe(recordPipe);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// set up a report streamer, which will read rows from the pipe, and write formatted report rows to the output stream //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ReportFormat reportFormat = reportInput.getReportFormat();
ReportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
reportStreamer.start(reportInput, getFields(reportInput));
ReportFormat reportFormat = exportInput.getReportFormat();
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
List<QFieldMetaData> fields = getFields(exportInput);
reportStreamer.start(exportInput, fields, "Sheet 1");
//////////////////////////////////////////
// run the query action as an async job //
//////////////////////////////////////////
AsyncJobManager asyncJobManager = new AsyncJobManager();
String queryJobUUID = asyncJobManager.startJob("ReportAction>QueryAction", (status) -> (queryInterface.execute(queryInput)));
String queryJobUUID = asyncJobManager.startJob("ReportAction>QueryAction", (status) -> (queryAction.execute(queryInput)));
LOG.info("Started query job [" + queryJobUUID + "] for report");
AsyncJobState queryJobState = AsyncJobState.RUNNING;
@ -207,8 +209,9 @@ public class ReportAction
lastReceivedRecordsAt = System.currentTimeMillis();
nextSleepMillis = INIT_SLEEP_MS;
int recordsConsumed = reportStreamer.takeRecordsFromPipe(recordPipe);
recordCount += recordsConsumed;
List<QRecord> records = recordPipe.consumeAvailableRecords();
processRecords(reportStreamer, fields, records);
recordCount += records.size();
LOG.info(countFromPreExecute != null
? String.format("Processed %,d of %,d records so far", recordCount, countFromPreExecute)
@ -235,8 +238,9 @@ public class ReportAction
///////////////////////////////////////////////////
// send the final records to the report streamer //
///////////////////////////////////////////////////
int recordsConsumed = reportStreamer.takeRecordsFromPipe(recordPipe);
recordCount += recordsConsumed;
List<QRecord> records = recordPipe.consumeAvailableRecords();
processRecords(reportStreamer, fields, records);
recordCount += records.size();
long reportEndTime = System.currentTimeMillis();
LOG.info((countFromPreExecute != null
@ -251,17 +255,17 @@ public class ReportAction
try
{
reportInput.getReportOutputStream().close();
exportInput.getReportOutputStream().close();
}
catch(Exception e)
{
throw (new QReportingException("Error completing report", e));
}
ReportOutput reportOutput = new ReportOutput();
reportOutput.setRecordCount(recordCount);
ExportOutput exportOutput = new ExportOutput();
exportOutput.setRecordCount(recordCount);
return (reportOutput);
return (exportOutput);
}
@ -269,17 +273,56 @@ public class ReportAction
/*******************************************************************************
**
*******************************************************************************/
private List<QFieldMetaData> getFields(ReportInput reportInput)
private static void processRecords(ExportStreamerInterface reportStreamer, List<QFieldMetaData> fields, List<QRecord> records) throws QReportingException
{
QTableMetaData table = reportInput.getTable();
if(reportInput.getFieldNames() != null)
for(QFieldMetaData field : fields)
{
return (reportInput.getFieldNames().stream().map(table::getField).toList());
if(field.getName().endsWith(":possibleValueLabel"))
{
String effectiveFieldName = field.getName().replace(":possibleValueLabel", "");
for(QRecord record : records)
{
String displayValue = record.getDisplayValue(effectiveFieldName);
record.setValue(field.getName(), displayValue);
}
}
}
reportStreamer.addRecords(records);
}
/*******************************************************************************
**
*******************************************************************************/
private List<QFieldMetaData> getFields(ExportInput exportInput)
{
List<QFieldMetaData> fieldList;
QTableMetaData table = exportInput.getTable();
if(exportInput.getFieldNames() != null)
{
fieldList = exportInput.getFieldNames().stream().map(table::getField).toList();
}
else
{
return (new ArrayList<>(table.getFields().values()));
fieldList = new ArrayList<>(table.getFields().values());
}
//////////////////////////////////////////
// add fields for possible value labels //
//////////////////////////////////////////
List<QFieldMetaData> returnList = new ArrayList<>();
for(QFieldMetaData field : fieldList)
{
returnList.add(field);
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{
returnList.add(new QFieldMetaData(field.getName() + ":possibleValueLabel", QFieldType.STRING).withLabel(field.getLabel() + " Name"));
}
}
return (returnList);
}
@ -287,11 +330,11 @@ public class ReportAction
/*******************************************************************************
**
*******************************************************************************/
private void verifyCountUnderMax(ReportInput reportInput, QBackendModuleInterface backendModule, ReportFormat reportFormat) throws QException
private void verifyCountUnderMax(ExportInput exportInput, QBackendModuleInterface backendModule, ReportFormat reportFormat) throws QException
{
if(reportFormat.getMaxCols() != null)
{
List<QFieldMetaData> fields = getFields(reportInput);
List<QFieldMetaData> fields = getFields(exportInput);
if(fields.size() > reportFormat.getMaxCols())
{
throw (new QUserFacingException("The requested report would include more columns ("
@ -302,13 +345,12 @@ public class ReportAction
if(reportFormat.getMaxRows() != null)
{
if(reportInput.getLimit() == null || reportInput.getLimit() > reportFormat.getMaxRows())
if(exportInput.getLimit() == null || exportInput.getLimit() > reportFormat.getMaxRows())
{
CountInterface countInterface = backendModule.getCountInterface();
CountInput countInput = new CountInput(reportInput.getInstance());
countInput.setSession(reportInput.getSession());
countInput.setTableName(reportInput.getTableName());
countInput.setFilter(reportInput.getQueryFilter());
CountInput countInput = new CountInput();
countInput.setTableName(exportInput.getTableName());
countInput.setFilter(exportInput.getQueryFilter());
CountOutput countOutput = countInterface.execute(countInput);
countFromPreExecute = countOutput.getCount();
if(countFromPreExecute > reportFormat.getMaxRows())

View File

@ -23,29 +23,46 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
/*******************************************************************************
** Interface for various report formats to implement.
** Interface for various export formats to implement.
*******************************************************************************/
public interface ReportStreamerInterface
public interface ExportStreamerInterface
{
/*******************************************************************************
** Called once, before any rows are available. Meant to write a header, for example.
*******************************************************************************/
void start(ReportInput reportInput, List<QFieldMetaData> fields) throws QReportingException;
void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException;
/*******************************************************************************
** Called as records flow into the pipe.
******************************************************************************/
int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException;
void addRecords(List<QRecord> recordList) throws QReportingException;
/*******************************************************************************
** Called once, after all rows are available. Meant to write a footer, or close resources, for example.
*******************************************************************************/
void finish() throws QReportingException;
/*******************************************************************************
**
*******************************************************************************/
default void setDisplayFormats(Map<String, String> displayFormats)
{
// noop in base class
}
/*******************************************************************************
**
*******************************************************************************/
default void addTotalsRow(QRecord record) throws QReportingException
{
addRecords(List.of(record));
}
}

View File

@ -0,0 +1,401 @@
/*
* 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.reporting;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Helper for Generating reports - to interpret formulas in report columns,
** that are in "excel-style", ala: =MINUS(47,42) or
** =IF(LT(ADD(${input.x},${input.y}),10,Yes,No)
*******************************************************************************/
public class FormulaInterpreter
{
/*******************************************************************************
** public method to interpret a formula. Takes a variableInterpreter, optionally
** full of maps of variables, and the formula string, assumed to have its leading
** '=' char already trimmed away.
*******************************************************************************/
public static Serializable interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula) throws QFormulaException
{
try
{
List<Serializable> results = interpretFormula(variableInterpreter, formula, new AtomicInteger(0));
if(results.size() == 1)
{
return (results.get(0));
}
else if(results.isEmpty())
{
throw (new QFormulaException("No results from formula"));
}
else
{
throw (new QFormulaException("More than 1 result from formula"));
}
}
catch(Exception e)
{
throw (new QFormulaException("Error interpreting formula [" + formula + "]", e));
}
}
/*******************************************************************************
** Recursive method that does the work of interpreting a formula.
** Uses AtomicInteger `i` to track index through the string into and out of
** recursive calls.
*******************************************************************************/
static List<Serializable> interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula, AtomicInteger i) throws QFormulaException
{
StringBuilder token = new StringBuilder();
List<Serializable> result = new ArrayList<>();
char previousChar = 0;
while(i.get() < formula.length())
{
if(i.get() > 0)
{
previousChar = formula.charAt(i.get() - 1);
}
char c = formula.charAt(i.getAndIncrement());
if(c == '(' && i.get() < formula.length() - 1)
{
//////////////////////////////////////////////////////////////////////////////////////////
// open paren means: go into a sub-parse. Get back a list of arguments, and use those //
// as arguments for the current token, which must be a function name then. //
//////////////////////////////////////////////////////////////////////////////////////////
List<Serializable> args = interpretFormula(variableInterpreter, formula, i);
Serializable evaluate = evaluate(token.toString(), args, variableInterpreter);
result.add(evaluate);
}
else if(c == ')')
{
//////////////////////////////////////////////////////////////////////////
// close paren means: end this sub-parse. evaluate the current token, //
// add it to the result list, and return the result list. //
// unless we just closed a paren - then we can just return. //
//////////////////////////////////////////////////////////////////////////
if(previousChar != ')')
{
Serializable evaluate = evaluate(token.toString(), Collections.emptyList(), variableInterpreter);
result.add(evaluate);
}
return (result);
}
else if(c == ',')
{
/////////////////////////////////////////////////////////////////////////
// comma means: evaluate the current token; add it to the result list //
// unless we just closed a paren - then we can just return. //
/////////////////////////////////////////////////////////////////////////
if(previousChar != ')')
{
Serializable evaluate = evaluate(token.toString(), Collections.emptyList(), variableInterpreter);
result.add(evaluate);
}
token = new StringBuilder();
}
else
{
/////////////////////////////////////////////////
// else, we add this char to the current token //
/////////////////////////////////////////////////
token.append(c);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we haven't found a result yet, assume we have just a literal, not a function call, and evaluate as such //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(result.isEmpty())
{
if(!token.isEmpty())
{
Serializable evaluate = evaluate(token.toString(), Collections.emptyList(), variableInterpreter);
result.add(evaluate);
}
}
return (result);
}
/*******************************************************************************
** Evaluate a token - maybe a literal, or variable, or function name -
** with arguments if it's a function, and in the context of the variableInterpreter.
*******************************************************************************/
private static Serializable evaluate(String token, List<Serializable> args, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
{
switch(token)
{
case "ADD":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).add(numbers.get(1)));
}
case "MINUS":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).subtract(numbers.get(1)));
}
case "MULTIPLY":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).multiply(numbers.get(1)));
}
case "DIVIDE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
if(numbers.get(1) == null || numbers.get(1).equals(BigDecimal.ZERO))
{
return null;
}
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).divide(numbers.get(1), 4, RoundingMode.HALF_UP));
}
case "DIVIDE_SCALE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 3, variableInterpreter);
if(numbers.get(1) == null || numbers.get(1).equals(BigDecimal.ZERO))
{
return null;
}
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).divide(numbers.get(1), numbers.get(2).intValue(), RoundingMode.HALF_UP));
}
case "ROUND":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).round(new MathContext(numbers.get(1).intValue())));
}
case "SCALE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).setScale(numbers.get(1).intValue(), RoundingMode.HALF_UP));
}
case "NVL":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return Objects.requireNonNullElse(numbers.get(0), numbers.get(1));
}
case "IF":
{
///////////////////////////////////////////////////////////////////////////////////////
// IF(CONDITION,TRUE,ELSE) //
// behavior in a spreadsheet appears to be: //
// booleans are evaluated naturally. //
// strings - if they look like 'true' or 'false, they are evaluated, else they error //
// numbers - 0 is false, all else are true. //
///////////////////////////////////////////////////////////////////////////////////////
List<Serializable> actualArgs = getArgumentList(args, 3, variableInterpreter);
Serializable condition = actualArgs.get(0);
boolean conditionBoolean;
if(condition == null)
{
conditionBoolean = false;
}
else if(condition instanceof Boolean b)
{
conditionBoolean = b;
}
else if(condition instanceof BigDecimal bd)
{
conditionBoolean = (bd.compareTo(BigDecimal.ZERO) != 0);
}
else if(condition instanceof String s)
{
if("true".equalsIgnoreCase(s))
{
conditionBoolean = true;
}
else if("false".equalsIgnoreCase(s))
{
conditionBoolean = false;
}
else
{
throw (new QFormulaException("Could not evaluate string '" + s + "' as a boolean."));
}
}
else
{
conditionBoolean = false;
}
return conditionBoolean ? actualArgs.get(1) : actualArgs.get(2);
}
case "LT":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) < 0);
}
case "LTE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) <= 0);
}
case "GT":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) > 0);
}
case "GTE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) >= 0);
}
default:
{
////////////////////////////////////////////////////////////////////////////////////////
// if there aren't arguments, then we can try to evaluate the thing not as a function //
////////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeIsEmpty(args))
{
try
{
return (ValueUtils.getValueAsBigDecimal(token));
}
catch(Exception e)
{
// continue
}
try
{
return (variableInterpreter.interpret(token));
}
catch(Exception e)
{
// continue
}
}
}
}
throw (new QFormulaException("Unable to evaluate unrecognized expression: " + token + ""));
}
/*******************************************************************************
** if any number in the list is null, get back null - else, return the result of the supplier.
*******************************************************************************/
private static Serializable nullIfAnyNullArgsElseBigDecimal(List<BigDecimal> numbers, Supplier<BigDecimal> supplier)
{
if(numbers.stream().anyMatch(Objects::isNull))
{
return (null);
}
return supplier.get();
}
/*******************************************************************************
** if any number in the list is null, get back null - else, return the result of the supplier.
*******************************************************************************/
private static Serializable nullIfAnyNullArgsElseBoolean(List<BigDecimal> numbers, Supplier<Boolean> supplier)
{
if(numbers.stream().anyMatch(Objects::isNull))
{
return (null);
}
return supplier.get();
}
/*******************************************************************************
** given a list of arguments, get back a specific number of arguments, all of which we
** validate to be numbers (e.g., possibly interpreted variables) - else we throw.
** also throw if not the right number is present.
*******************************************************************************/
private static List<BigDecimal> getNumberArgumentList(List<Serializable> originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
{
if(howMany != null)
{
if(!howMany.equals(originalArgs.size()))
{
throw (new QFormulaException("Wrong number of arguments (required: " + howMany + ", received: " + originalArgs.size() + ")"));
}
}
List<BigDecimal> rs = new ArrayList<>();
for(Serializable originalArg : originalArgs)
{
try
{
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null);
rs.add(ValueUtils.getValueAsBigDecimal(interpretedArg));
}
catch(QValueException e)
{
throw (new QFormulaException("Could not process [" + originalArg + "] as a number"));
}
}
return (rs);
}
/*******************************************************************************
** given a list of arguments, get back a specific number of arguments, all of which we
** get interpreted. throw if not the right number of args is present.
*******************************************************************************/
private static List<Serializable> getArgumentList(List<Serializable> originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
{
if(howMany != null)
{
if(!howMany.equals(originalArgs.size()))
{
throw (new QFormulaException("Wrong number of arguments (required: " + howMany + ", received: " + originalArgs.size() + ")"));
}
}
List<Serializable> rs = new ArrayList<>();
for(Serializable originalArg : originalArgs)
{
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null);
rs.add(interpretedArg);
}
return (rs);
}
}

View File

@ -0,0 +1,901 @@
/*
* 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.reporting;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.DataSourceQueryInputCustomizer;
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportViewCustomizer;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
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;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
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 com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.aggregates.AggregatesInterface;
import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
/*******************************************************************************
** Action to generate a report.
**
** A report can contain 1 or more Data Sources - e.g., tables + filters that define
** data that goes into the report, or simple data-supplier lambdas.
**
** A report can also contain 1 or more Views - e.g., sheets in a spreadsheet workbook.
** (how do those work in non-XLSX formats??). Views can either be:
** - plain tables,
** - summaries (like pivot tables, but called summary to avoid confusion with "native" pivot tables),
** - native pivot tables (not initially supported, due to lack of support in fastexcel...).
*******************************************************************************/
public class GenerateReportAction
{
private static final QLogger LOG = QLogger.getLogger(GenerateReportAction.class);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// summaryAggregates and varianceAggregates are multi-level maps, ala: //
// viewName > SummaryKey > fieldName > Aggregates //
// e.g.: //
// viewName: salesSummaryReport //
// SummaryKey: [(state:MO),(city:St.Louis)] //
// fieldName: salePrice //
// Aggregates: (count:47;sum:10,000;max:2,000;min:15) //
// salesSummaryReport > [(state:MO),(city:St.Louis)] > salePrice > (count:47;sum:10,000;max:2,000;min:15) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> summaryAggregates = new HashMap<>();
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> varianceAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> totalAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> varianceTotalAggregates = new HashMap<>();
private QReportMetaData report;
private ReportFormat reportFormat;
private ExportStreamerInterface reportStreamer;
/*******************************************************************************
**
*******************************************************************************/
public void execute(ReportInput reportInput) throws QException
{
report = reportInput.getInstance().getReport(reportInput.getReportName());
reportFormat = reportInput.getReportFormat();
if(reportFormat == null)
{
throw new QException("Report format was not specified.");
}
reportStreamer = reportFormat.newReportStreamer();
////////////////////////////////////////////////////////////////////////////////////////////////
// foreach data source, do a query (possibly more than 1, if it goes to multiple table views) //
////////////////////////////////////////////////////////////////////////////////////////////////
for(QReportDataSource dataSource : report.getDataSources())
{
//////////////////////////////////////////////////////////////////////////////
// make a list of the views that use this data source for various purposes. //
//////////////////////////////////////////////////////////////////////////////
List<QReportView> dataSourceTableViews = report.getViews().stream()
.filter(v -> v.getType().equals(ReportType.TABLE))
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList();
List<QReportView> dataSourceSummaryViews = report.getViews().stream()
.filter(v -> v.getType().equals(ReportType.SUMMARY))
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList();
List<QReportView> dataSourceVariantViews = report.getViews().stream()
.filter(v -> v.getType().equals(ReportType.SUMMARY))
.filter(v -> v.getVarianceDataSourceName() != null && v.getVarianceDataSourceName().equals(dataSource.getName()))
.toList();
/////////////////////////////////////////////////////////////////////////////////////////////
// if this data source isn't used for any table views, but it is used for one or //
// more summary views (possibly as a variant), then run the query, gathering summary data. //
/////////////////////////////////////////////////////////////////////////////////////////////
if(dataSourceTableViews.isEmpty())
{
if(!dataSourceSummaryViews.isEmpty() || !dataSourceVariantViews.isEmpty())
{
gatherData(reportInput, dataSource, null, dataSourceSummaryViews, dataSourceVariantViews);
}
}
else
{
////////////////////////////////////////////////////////////////////////////////////////
// else, foreach table view this data source is used for, run the data source's query //
////////////////////////////////////////////////////////////////////////////////////////
for(QReportView dataSourceTableView : dataSourceTableViews)
{
/////////////////////////////////////////////////////////////////////////////////////////
// if there's a view customizer, run it (e.g., to customize the columns in the report) //
/////////////////////////////////////////////////////////////////////////////////////////
if(dataSourceTableView.getViewCustomizer() != null)
{
Function<QReportView, QReportView> viewCustomizerFunction = QCodeLoader.getFunction(dataSourceTableView.getViewCustomizer());
if(viewCustomizerFunction instanceof ReportViewCustomizer reportViewCustomizer)
{
reportViewCustomizer.setReportInput(reportInput);
}
dataSourceTableView = viewCustomizerFunction.apply(dataSourceTableView.clone());
}
////////////////////////////////////////////////////////////////////////////////////
// start the table-view (e.g., open this tab in xlsx) and then run the query-loop //
////////////////////////////////////////////////////////////////////////////////////
startTableView(reportInput, dataSource, dataSourceTableView);
gatherData(reportInput, dataSource, dataSourceTableView, dataSourceSummaryViews, dataSourceVariantViews);
}
}
}
outputSummaries(reportInput);
reportStreamer.finish();
try
{
reportInput.getReportOutputStream().close();
}
catch(Exception e)
{
throw (new QReportingException("Error completing report", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView) throws QException
{
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues());
ExportInput exportInput = new ExportInput();
exportInput.setReportFormat(reportFormat);
exportInput.setFilename(reportInput.getFilename());
exportInput.setTitleRow(getTitle(reportView, variableInterpreter));
exportInput.setIncludeHeaderRow(reportView.getIncludeHeaderRow());
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
JoinsContext joinsContext = null;
if(StringUtils.hasContent(dataSource.getSourceTable()))
{
joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
}
List<QFieldMetaData> fields = new ArrayList<>();
for(QReportField column : reportView.getColumns())
{
if(column.getIsVirtual())
{
fields.add(column.toField());
}
else
{
String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName());
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext == null ? null : joinsContext.getFieldAndTableNameOrAlias(effectiveFieldName);
if(fieldAndTableNameOrAlias == null || fieldAndTableNameOrAlias.field() == null)
{
throw new QReportingException("Could not find field named [" + effectiveFieldName + "] in dataSource [" + dataSource.getName() + "]");
}
QFieldMetaData field = fieldAndTableNameOrAlias.field().clone();
field.setName(column.getName());
if(StringUtils.hasContent(column.getLabel()))
{
field.setLabel(column.getLabel());
}
fields.add(field);
}
}
reportStreamer.setDisplayFormats(getDisplayFormatMap(fields));
reportStreamer.start(exportInput, fields, reportView.getLabel());
}
/*******************************************************************************
**
*******************************************************************************/
private void gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
{
////////////////////////////////////////////////////////////////////////////////////////
// check if this view has a transform step - if so, set it up now and run its pre-run //
////////////////////////////////////////////////////////////////////////////////////////
AbstractTransformStep transformStep = null;
RunBackendStepInput transformStepInput = null;
RunBackendStepOutput transformStepOutput = null;
if(tableView != null && tableView.getRecordTransformStep() != null)
{
transformStep = QCodeLoader.getBackendStep(AbstractTransformStep.class, tableView.getRecordTransformStep());
transformStepInput = new RunBackendStepInput();
transformStepInput.setValues(reportInput.getInputValues());
transformStepOutput = new RunBackendStepOutput();
transformStep.preRun(transformStepInput, transformStepOutput);
}
////////////////////////////////////////////////////////////////////
// create effectively-final versions of these vars for the lambda //
////////////////////////////////////////////////////////////////////
AbstractTransformStep finalTransformStep = transformStep;
RunBackendStepInput finalTransformStepInput = transformStepInput;
RunBackendStepOutput finalTransformStepOutput = transformStepOutput;
/////////////////////////////////////////////////////////////////
// run a record pipe loop, over the query for this data source //
/////////////////////////////////////////////////////////////////
RecordPipe recordPipe = new BufferedRecordPipe(1000);
new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) ->
{
if(dataSource.getSourceTable() != null)
{
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
setInputValuesInQueryFilter(reportInput, queryFilter);
QueryInput queryInput = new QueryInput();
queryInput.setRecordPipe(recordPipe);
queryInput.setTableName(dataSource.getSourceTable());
queryInput.setFilter(queryFilter);
queryInput.setQueryJoins(dataSource.getQueryJoins());
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource, new JoinsContext(reportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryInput.getFilter())));
if(dataSource.getQueryInputCustomizer() != null)
{
DataSourceQueryInputCustomizer queryInputCustomizer = QCodeLoader.getAdHoc(DataSourceQueryInputCustomizer.class, dataSource.getQueryInputCustomizer());
queryInput = queryInputCustomizer.run(reportInput, queryInput);
}
return (new QueryAction().execute(queryInput));
}
else if(dataSource.getStaticDataSupplier() != null)
{
@SuppressWarnings("unchecked")
Supplier<List<List<Serializable>>> supplier = QCodeLoader.getAdHoc(Supplier.class, dataSource.getStaticDataSupplier());
List<List<Serializable>> lists = supplier.get();
for(List<Serializable> list : lists)
{
QRecord record = new QRecord();
int index = 0;
for(Serializable value : list)
{
record.setValue("column" + (index++), value);
}
recordPipe.addRecord(record);
}
return (true);
}
else
{
throw (new IllegalStateException("Misconfigured data source [" + dataSource.getName() + "]."));
}
}, () ->
{
List<QRecord> records = recordPipe.consumeAvailableRecords();
if(finalTransformStep != null)
{
finalTransformStepInput.setRecords(records);
finalTransformStep.run(finalTransformStepInput, finalTransformStepOutput);
records = finalTransformStepOutput.getRecords();
}
return (consumeRecords(reportInput, dataSource, records, tableView, summaryViews, variantViews));
});
////////////////////////////////////////////////
// if there's a transformer, run its post-run //
////////////////////////////////////////////////
if(transformStep != null)
{
transformStep.postRun(transformStepInput, transformStepOutput);
}
}
/*******************************************************************************
**
*******************************************************************************/
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext)
{
Set<String> fieldsToTranslatePossibleValues = new HashSet<>();
for(QReportView view : report.getViews())
{
for(QReportField column : CollectionUtils.nonNullList(view.getColumns()))
{
////////////////////////////////////////////////////////////////////////////////////////
// if this is a column marked as ShowPossibleValueLabel, then we need to translate it //
////////////////////////////////////////////////////////////////////////////////////////
if(column.getShowPossibleValueLabel())
{
String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName());
fieldsToTranslatePossibleValues.add(effectiveFieldName);
}
}
for(String summaryField : CollectionUtils.nonNullList(view.getPivotFields()))
{
///////////////////////////////////////////////////////////////////////////////
// all pivotFields that are possible value sources are implicitly translated //
///////////////////////////////////////////////////////////////////////////////
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
if(table.getField(summaryField).getPossibleValueSourceName() != null)
{
fieldsToTranslatePossibleValues.add(summaryField);
}
}
}
return (fieldsToTranslatePossibleValues);
}
/*******************************************************************************
**
*******************************************************************************/
private void setInputValuesInQueryFilter(ReportInput reportInput, QQueryFilter queryFilter)
{
if(queryFilter == null || queryFilter.getCriteria() == null)
{
return;
}
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues());
for(QFilterCriteria criterion : queryFilter.getCriteria())
{
if(criterion.getValues() != null)
{
List<Serializable> newValues = new ArrayList<>();
for(Serializable value : criterion.getValues())
{
String valueAsString = ValueUtils.getValueAsString(value);
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
newValues.add(interpretedValue);
}
criterion.setValues(newValues);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private Integer consumeRecords(ReportInput reportInput, QReportDataSource dataSource, List<QRecord> records, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
{
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
////////////////////////////////////////////////////////////////////////////
// if this record goes on a table view, add it to the report streamer now //
////////////////////////////////////////////////////////////////////////////
if(tableView != null)
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if any fields are 'showPossibleValueLabel', then move display values for them into the record's values map //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QReportField column : tableView.getColumns())
{
if(column.getShowPossibleValueLabel())
{
String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName());
for(QRecord record : records)
{
String displayValue = record.getDisplayValue(effectiveFieldName);
record.setValue(column.getName(), displayValue);
}
}
}
reportStreamer.addRecords(records);
}
/////////////////////////////////
// do aggregates for summaries //
/////////////////////////////////
if(summaryViews != null)
{
for(QReportView summaryView : summaryViews)
{
addRecordsToSummaryAggregates(summaryView, table, records, summaryAggregates);
}
}
if(variantViews != null)
{
for(QReportView variantView : variantViews)
{
addRecordsToSummaryAggregates(variantView, table, records, varianceAggregates);
}
}
///////////////////////////////////////////
// do totals too, if any views want them //
///////////////////////////////////////////
if(summaryViews != null && summaryViews.stream().anyMatch(QReportView::getIncludeTotalRow))
{
for(QRecord record : records)
{
addRecordToAggregatesMap(table, record, totalAggregates);
}
}
if(variantViews != null && variantViews.stream().anyMatch(QReportView::getIncludeTotalRow))
{
for(QRecord record : records)
{
addRecordToAggregatesMap(table, record, varianceTotalAggregates);
}
}
return (records.size());
}
/*******************************************************************************
**
*******************************************************************************/
private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> aggregatesMap)
{
Map<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
for(QRecord record : records)
{
SummaryKey key = new SummaryKey();
for(String summaryField : view.getPivotFields())
{
Serializable summaryValue = record.getValue(summaryField);
if(table.getField(summaryField).getPossibleValueSourceName() != null)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// so, this is kinda a thing - where we implicitly use possible-value labels (e.g., display values) for pivot fields... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
summaryValue = record.getDisplayValue(summaryField);
}
key.add(summaryField, summaryValue);
if(view.getIncludePivotSubTotals() && key.getKeys().size() < view.getPivotFields().size())
{
/////////////////////////////////////////////////////////////////////////////////////////
// be careful here, with these key objects, and their identity, being used as map keys //
/////////////////////////////////////////////////////////////////////////////////////////
SummaryKey subKey = key.clone();
addRecordToSummaryKeyAggregates(table, record, viewAggregates, subKey);
}
}
addRecordToSummaryKeyAggregates(table, record, viewAggregates, key);
}
}
/*******************************************************************************
**
*******************************************************************************/
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);
}
/*******************************************************************************
**
*******************************************************************************/
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?>> aggregatesMap)
{
for(QFieldMetaData field : table.getFields().values())
{
if(field.getType().equals(QFieldType.INTEGER))
{
@SuppressWarnings("unchecked")
AggregatesInterface<Integer> fieldAggregates = (AggregatesInterface<Integer>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
fieldAggregates.add(record.getValueInteger(field.getName()));
}
else if(field.getType().equals(QFieldType.DECIMAL))
{
@SuppressWarnings("unchecked")
AggregatesInterface<BigDecimal> fieldAggregates = (AggregatesInterface<BigDecimal>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates());
fieldAggregates.add(record.getValueBigDecimal(field.getName()));
}
// todo - more types (dates, at least?)
}
}
/*******************************************************************************
**
*******************************************************************************/
private void outputSummaries(ReportInput reportInput) throws QReportingException, QFormulaException
{
List<QReportView> reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
for(QReportView view : reportViews)
{
QReportDataSource dataSource = report.getDataSource(view.getDataSourceName());
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
ExportInput exportInput = new ExportInput();
exportInput.setReportFormat(reportFormat);
exportInput.setFilename(reportInput.getFilename());
exportInput.setTitleRow(summaryOutput.titleRow);
exportInput.setIncludeHeaderRow(view.getIncludeHeaderRow());
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
reportStreamer.setDisplayFormats(getDisplayFormatMap(view));
reportStreamer.start(exportInput, getFields(table, view), view.getLabel());
reportStreamer.addRecords(summaryOutput.summaryRows); // todo - what if this set is huge?
if(summaryOutput.totalRow != null)
{
reportStreamer.addTotalsRow(summaryOutput.totalRow);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private Map<String, String> getDisplayFormatMap(QReportView view)
{
return (view.getColumns().stream()
.filter(c -> c.getDisplayFormat() != null)
.collect(Collectors.toMap(QReportField::getName, QReportField::getDisplayFormat)));
}
/*******************************************************************************
**
*******************************************************************************/
private Map<String, String> getDisplayFormatMap(List<QFieldMetaData> fields)
{
return (fields.stream()
.filter(f -> f.getDisplayFormat() != null)
.collect(Collectors.toMap(QFieldMetaData::getName, QFieldMetaData::getDisplayFormat)));
}
/*******************************************************************************
**
*******************************************************************************/
private List<QFieldMetaData> getFields(QTableMetaData table, QReportView view)
{
List<QFieldMetaData> fields = new ArrayList<>();
for(String pivotField : view.getPivotFields())
{
QFieldMetaData field = table.getField(pivotField);
fields.add(new QFieldMetaData(pivotField, field.getType()).withLabel(field.getLabel())); // todo do we need the type? if so need table as input here
}
for(QReportField column : view.getColumns())
{
fields.add(new QFieldMetaData().withName(column.getName()).withLabel(column.getLabel())); // todo do we need the type? if so need table as input here
}
return (fields);
}
/*******************************************************************************
**
*******************************************************************************/
private SummaryOutput computeSummaryRowsForView(ReportInput reportInput, QReportView view, QTableMetaData table) throws QReportingException, QFormulaException
{
QValueFormatter valueFormatter = new QValueFormatter();
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues());
variableInterpreter.addValueMap("total", getSummaryValuesForInterpreter(totalAggregates));
///////////
// title //
///////////
String title = getTitle(view, variableInterpreter);
/////////////////////////
// create summary rows //
/////////////////////////
List<QRecord> summaryRows = new ArrayList<>();
for(Map.Entry<SummaryKey, Map<String, AggregatesInterface<?>>> entry : summaryAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
{
SummaryKey summaryKey = entry.getKey();
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue();
Map<String, Serializable> summaryValues = getSummaryValuesForInterpreter(fieldAggregates);
variableInterpreter.addValueMap("pivot", summaryValues);
variableInterpreter.addValueMap("summary", summaryValues);
HashMap<String, Serializable> thisRowValues = new HashMap<>();
variableInterpreter.addValueMap("thisRow", thisRowValues);
if(!varianceAggregates.isEmpty())
{
Map<SummaryKey, Map<String, AggregatesInterface<?>>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap());
Map<String, AggregatesInterface<?>> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap());
Map<String, Serializable> varianceValues = getSummaryValuesForInterpreter(varianceSubMap);
variableInterpreter.addValueMap("variancePivot", varianceValues);
variableInterpreter.addValueMap("variance", varianceValues);
}
QRecord summaryRow = new QRecord();
summaryRows.add(summaryRow);
////////////////////////////
// add the summary values //
////////////////////////////
for(Pair<String, Serializable> key : summaryKey.getKeys())
{
summaryRow.setValue(key.getA(), key.getB());
}
///////////////////////////////////////////////////////////////////////////////
// for summary subtotals, add the text "Total" to the last field in this key //
///////////////////////////////////////////////////////////////////////////////
if(summaryKey.getKeys().size() < view.getPivotFields().size())
{
String fieldName = summaryKey.getKeys().get(summaryKey.getKeys().size() - 1).getA();
summaryRow.setValue(fieldName, summaryRow.getValueString(fieldName) + " Total");
}
///////////////////////////
// add the column values //
///////////////////////////
for(QReportField column : view.getColumns())
{
Serializable serializable = getValueForColumn(variableInterpreter, column);
summaryRow.setValue(column.getName(), serializable);
thisRowValues.put(column.getName(), serializable);
}
}
//////////////////////////////////////////////////////////////////////////////////////
// sort the summary rows //
// Note - this will NOT work correctly if there's more than 1 pivot field, as we're //
// not doing anything to keep related rows them together (e.g., all MO state rows) //
//////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(view.getOrderByFields()))
{
summaryRows.sort((o1, o2) ->
{
return summaryRowComparator(view, o1, o2);
});
}
////////////////
// totals row //
////////////////
QRecord totalRow = null;
if(view.getIncludeTotalRow())
{
totalRow = new QRecord();
for(String pivotField : view.getPivotFields())
{
if(totalRow.getValues().isEmpty())
{
totalRow.setValue(pivotField, "Totals");
}
}
Map<String, Serializable> totalValues = getSummaryValuesForInterpreter(totalAggregates);
variableInterpreter.addValueMap("pivot", totalValues);
variableInterpreter.addValueMap("summary", totalValues);
Map<String, Serializable> varianceTotalValues = getSummaryValuesForInterpreter(varianceTotalAggregates);
variableInterpreter.addValueMap("variancePivot", varianceTotalValues);
variableInterpreter.addValueMap("variance", varianceTotalValues);
HashMap<String, Serializable> thisRowValues = new HashMap<>();
variableInterpreter.addValueMap("thisRow", thisRowValues);
for(QReportField column : view.getColumns())
{
Serializable serializable = getValueForColumn(variableInterpreter, column);
totalRow.setValue(column.getName(), serializable);
thisRowValues.put(column.getName(), serializable);
String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable);
}
}
return (new SummaryOutput(summaryRows, title, totalRow));
}
/*******************************************************************************
**
*******************************************************************************/
private String getTitle(QReportView view, QMetaDataVariableInterpreter variableInterpreter)
{
String title = null;
if(view.getTitleFields() != null && StringUtils.hasContent(view.getTitleFormat()))
{
List<String> titleValues = new ArrayList<>();
for(String titleField : view.getTitleFields())
{
titleValues.add(variableInterpreter.interpret(titleField));
}
title = new QValueFormatter().formatStringWithValues(view.getTitleFormat(), titleValues);
}
else if(StringUtils.hasContent(view.getTitleFormat()))
{
title = view.getTitleFormat();
}
return title;
}
/*******************************************************************************
**
*******************************************************************************/
private Serializable getValueForColumn(QMetaDataVariableInterpreter variableInterpreter, QReportField column) throws QFormulaException
{
String formula = column.getFormula();
Serializable result;
if(formula.startsWith("=") && formula.length() > 1)
{
result = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1));
}
else
{
result = variableInterpreter.interpretForObject(formula, null);
}
return (result);
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings({ "rawtypes", "unchecked" })
private int summaryRowComparator(QReportView view, QRecord o1, QRecord o2)
{
if(o1 == o2)
{
return (0);
}
for(QFilterOrderBy orderByField : view.getOrderByFields())
{
Comparable c1 = (Comparable) o1.getValue(orderByField.getFieldName());
Comparable c2 = (Comparable) o2.getValue(orderByField.getFieldName());
if(c1 == null && c2 == null)
{
continue;
}
if(c1 == null)
{
return (orderByField.getIsAscending() ? -1 : 1);
}
if(c2 == null)
{
return (orderByField.getIsAscending() ? 1 : -1);
}
int comp = orderByField.getIsAscending() ? c1.compareTo(c2) : c2.compareTo(c1);
if(comp != 0)
{
return (comp);
}
}
return (0);
}
/*******************************************************************************
**
*******************************************************************************/
private Map<String, Serializable> getSummaryValuesForInterpreter(Map<String, AggregatesInterface<?>> fieldAggregates)
{
Map<String, Serializable> summaryValuesForInterpreter = new HashMap<>();
for(Map.Entry<String, AggregatesInterface<?>> subEntry : fieldAggregates.entrySet())
{
String fieldName = subEntry.getKey();
AggregatesInterface<?> aggregates = subEntry.getValue();
summaryValuesForInterpreter.put("sum." + fieldName, aggregates.getSum());
summaryValuesForInterpreter.put("count." + fieldName, aggregates.getCount());
summaryValuesForInterpreter.put("min." + fieldName, aggregates.getMin());
summaryValuesForInterpreter.put("max." + fieldName, aggregates.getMax());
summaryValuesForInterpreter.put("average." + fieldName, aggregates.getAverage());
}
return summaryValuesForInterpreter;
}
/*******************************************************************************
** record to serve as tuple/multi-value output of computeSummaryRowsForView method.
*******************************************************************************/
private record SummaryOutput(List<QRecord> summaryRows, String titleRow, QRecord totalRow)
{
}
}

View File

@ -0,0 +1,177 @@
/*
* 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.reporting;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** JSON export format implementation
*******************************************************************************/
public class JsonExportStreamer implements ExportStreamerInterface
{
private static final QLogger LOG = QLogger.getLogger(JsonExportStreamer.class);
private ExportInput exportInput;
private QTableMetaData table;
private List<QFieldMetaData> fields;
private OutputStream outputStream;
private boolean needComma = false;
private boolean prettyPrint = true;
/*******************************************************************************
**
*******************************************************************************/
public JsonExportStreamer()
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
{
this.exportInput = exportInput;
this.fields = fields;
table = exportInput.getTable();
outputStream = this.exportInput.getReportOutputStream();
try
{
outputStream.write('[');
}
catch(IOException e)
{
throw (new QReportingException("Error starting report output", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecords(List<QRecord> qRecords) throws QReportingException
{
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
for(QRecord qRecord : qRecords)
{
writeRecord(qRecord);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void writeRecord(QRecord qRecord) throws QReportingException
{
try
{
if(needComma)
{
outputStream.write(',');
}
Map<String, Serializable> mapForJson = new LinkedHashMap<>();
for(QFieldMetaData field : fields)
{
String labelForJson = StringUtils.lcFirst(field.getLabel().replace(" ", ""));
mapForJson.put(labelForJson, qRecord.getValue(field.getName()));
}
String json = prettyPrint ? JsonUtils.toPrettyJson(mapForJson) : JsonUtils.toJson(mapForJson);
if(prettyPrint)
{
outputStream.write('\n');
}
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush(); // todo - less often?
needComma = true;
}
catch(Exception e)
{
throw (new QReportingException("Error writing JSON report", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addTotalsRow(QRecord record) throws QReportingException
{
writeRecord(record);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void finish() throws QReportingException
{
try
{
if(prettyPrint)
{
outputStream.write('\n');
}
outputStream.write(']');
}
catch(IOException e)
{
throw (new QReportingException("Error ending report output", e));
}
}
}

View File

@ -0,0 +1,161 @@
/*
* 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.reporting;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
/*******************************************************************************
** Report streamer implementation that just builds up a STATIC list of lists of strings.
** Meant only for use in unit tests at this time... would need refactored for
** multi-thread/multi-use if wanted for real usage.
*******************************************************************************/
public class ListOfMapsExportStreamer implements ExportStreamerInterface
{
private static final QLogger LOG = QLogger.getLogger(ListOfMapsExportStreamer.class);
private ExportInput exportInput;
private List<QFieldMetaData> fields;
private static Map<String, List<Map<String, String>>> rows = new LinkedHashMap<>();
private static Map<String, List<String>> headers = new LinkedHashMap<>();
private static String currentSheetLabel;
/*******************************************************************************
**
*******************************************************************************/
public ListOfMapsExportStreamer()
{
}
/*******************************************************************************
**
*******************************************************************************/
public static void reset()
{
rows.clear();
headers.clear();
currentSheetLabel = null;
}
/*******************************************************************************
** Getter for list
**
*******************************************************************************/
public static List<Map<String, String>> getList(String name)
{
return (rows.get(name));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
{
this.exportInput = exportInput;
this.fields = fields;
currentSheetLabel = label;
rows.put(label, new ArrayList<>());
if(exportInput.getIncludeHeaderRow())
{
headers.put(label, new ArrayList<>());
for(QFieldMetaData field : fields)
{
headers.get(label).add(field.getLabel());
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecords(List<QRecord> qRecords) throws QReportingException
{
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
for(QRecord qRecord : qRecords)
{
addRecord(qRecord);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void addRecord(QRecord qRecord)
{
Map<String, String> row = new LinkedHashMap<>();
rows.get(currentSheetLabel).add(row);
for(int i = 0; i < fields.size(); i++)
{
row.put(headers.get(currentSheetLabel).get(i), qRecord.getValueString(fields.get(i).getName()));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addTotalsRow(QRecord record)
{
addRecord(record);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void finish()
{
}
}

View File

@ -27,10 +27,9 @@ import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -39,10 +38,15 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class RecordPipe
{
private static final Logger LOG = LogManager.getLogger(RecordPipe.class);
private static final QLogger LOG = QLogger.getLogger(RecordPipe.class);
private static final long BLOCKING_SLEEP_MILLIS = 100;
private static final long MAX_SLEEP_LOOP_MILLIS = 300_000; // 5 minutes
private ArrayBlockingQueue<QRecord> queue = new ArrayBlockingQueue<>(1_000);
private boolean isTerminated = false;
private Consumer<List<QRecord>> postRecordActions = null;
/////////////////////////////////////
@ -51,11 +55,51 @@ public class RecordPipe
private List<QRecord> singleRecordListForPostRecordActions = new ArrayList<>();
/*******************************************************************************
** Add a record to the pipe. Will block if the pipe is full.
** Default constructor.
*******************************************************************************/
public RecordPipe()
{
}
/*******************************************************************************
** Construct a record pipe, with an alternative capacity for the internal queue.
*******************************************************************************/
public RecordPipe(Integer overrideCapacity)
{
queue = new ArrayBlockingQueue<>(overrideCapacity);
}
/*******************************************************************************
** Turn off the pipe. Stop accepting new records (just ignore them in the add
** method). Clear the existing queue. Don't return any more records. Note that
** if consumeAvailableRecords was running in another thread, it may still return
** some records that it read before this call.
*******************************************************************************/
public void terminate()
{
isTerminated = true;
queue.clear();
}
/*******************************************************************************
** Add a record to the pipe. Will block if the pipe is full. Will noop if pipe is terminated.
*******************************************************************************/
public void addRecord(QRecord record)
{
if(isTerminated)
{
return;
}
if(postRecordActions != null)
{
////////////////////////////////////////////////////////////////////////////////////
@ -82,18 +126,31 @@ public class RecordPipe
{
boolean offerResult = queue.offer(record);
while(!offerResult)
if(!offerResult && !isTerminated)
{
LOG.debug("Record pipe.add failed (due to full pipe). Blocking.");
SleepUtils.sleep(100, TimeUnit.MILLISECONDS);
LOG.debug("Pipe is full. Waiting.");
long sleepLoopStartTime = System.currentTimeMillis();
long now = System.currentTimeMillis();
while(!offerResult && !isTerminated)
{
if(now - sleepLoopStartTime > MAX_SLEEP_LOOP_MILLIS)
{
LOG.warn("Giving up adding record to pipe, due to pipe being full for more than {} millis", MAX_SLEEP_LOOP_MILLIS);
throw (new IllegalStateException("Giving up adding record to pipe, due to pipe staying full too long."));
}
LOG.trace("Record pipe.add failed (due to full pipe). Blocking.");
SleepUtils.sleep(BLOCKING_SLEEP_MILLIS, TimeUnit.MILLISECONDS);
offerResult = queue.offer(record);
now = System.currentTimeMillis();
}
LOG.debug("Pipe has opened up. Resuming.");
}
}
/*******************************************************************************
** Add a list of records to the pipe
** Add a list of records to the pipe. Will block if the pipe is full. Will noop if pipe is terminated.
*******************************************************************************/
public void addRecords(List<QRecord> records)
{
@ -117,7 +174,7 @@ public class RecordPipe
{
List<QRecord> rs = new ArrayList<>();
while(true)
while(!isTerminated)
{
QRecord record = queue.poll();
if(record == null)
@ -137,6 +194,11 @@ public class RecordPipe
*******************************************************************************/
public int countAvailableRecords()
{
if(isTerminated)
{
return (0);
}
return (queue.size());
}

View File

@ -0,0 +1,131 @@
/*
* 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.reporting;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.utils.Pair;
/*******************************************************************************
** For a summary report, a list of field/value pairs that make up a "key".
**
** For example, in a report doing summaries by State > City > ZipCode, a SummaryKey
** would look like: [(state:MO),(city:St.Louis),(zipCode:63101)].
*******************************************************************************/
public class SummaryKey implements Cloneable
{
private List<Pair<String, Serializable>> keys = new ArrayList<>();
/*******************************************************************************
**
*******************************************************************************/
public SummaryKey()
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "PivotKey{keys=" + keys + '}';
}
/*******************************************************************************
**
*******************************************************************************/
public void add(String field, Serializable value)
{
keys.add(new Pair<>(field, value));
}
/*******************************************************************************
** Getter for keys
**
*******************************************************************************/
public List<Pair<String, Serializable>> getKeys()
{
return keys;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean equals(Object o)
{
if(this == o)
{
return true;
}
if(o == null || getClass() != o.getClass())
{
return false;
}
SummaryKey summaryKey = (SummaryKey) o;
return Objects.equals(keys, summaryKey.keys);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public int hashCode()
{
return Objects.hash(keys);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public SummaryKey clone()
{
SummaryKey clone = new SummaryKey();
for(Pair<String, Serializable> key : keys)
{
clone.add(key.getA(), key.getB());
}
return (clone);
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.reporting.customizers;
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.actions.tables.query.QueryInput;
/*******************************************************************************
** Interface for customizer on a QReportDataSource's query.
**
** Useful, for example, to look at what input field values were given, and change
** the query filter (e.g., conditional criteria), or issue an error based on the
** combination of input fields given.
*******************************************************************************/
public interface DataSourceQueryInputCustomizer
{
/*******************************************************************************
**
*******************************************************************************/
QueryInput run(ReportInput reportInput, QueryInput queryInput) throws QException;
}

View File

@ -0,0 +1,42 @@
/*
* 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.reporting.customizers;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
/*******************************************************************************
** Interface for customizer on a QReportView. Extends Function by adding setter
** method for reportInput.
*******************************************************************************/
public interface ReportViewCustomizer extends Function<QReportView, QReportView>
{
/*******************************************************************************
**
*******************************************************************************/
void setReportInput(ReportInput reportInput);
}

View File

@ -0,0 +1,71 @@
/*
* 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.reporting.excelformatting;
import org.dhatim.fastexcel.BorderSide;
import org.dhatim.fastexcel.BorderStyle;
import org.dhatim.fastexcel.StyleSetter;
/*******************************************************************************
** Version of excel styler that does bold headers and footers, with basic borders.
*******************************************************************************/
public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void styleTitleRow(StyleSetter titleRowStyle)
{
titleRowStyle
.bold()
.fontSize(14)
.horizontalAlignment("center");
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void styleHeaderRow(StyleSetter headerRowStyle)
{
headerRowStyle
.bold()
.borderStyle(BorderSide.BOTTOM, BorderStyle.THIN);
}
@Override
public void styleTotalsRow(StyleSetter totalsRowStyle)
{
totalsRowStyle
.bold()
.borderStyle(BorderSide.TOP, BorderStyle.THIN)
.borderStyle(BorderSide.BOTTOM, BorderStyle.DOUBLE);
}
}

View File

@ -0,0 +1,59 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
import org.dhatim.fastexcel.StyleSetter;
/*******************************************************************************
** Interface for classes that know how to apply styles to an Excel stream being
** built by fastexcel.
*******************************************************************************/
public interface ExcelStylerInterface
{
/*******************************************************************************
**
*******************************************************************************/
default void styleTitleRow(StyleSetter titleRowStyle)
{
}
/*******************************************************************************
**
*******************************************************************************/
default void styleHeaderRow(StyleSetter headerRowStyle)
{
}
/*******************************************************************************
**
*******************************************************************************/
default void styleTotalsRow(StyleSetter totalsRowStyle)
{
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.reporting.excelformatting;
/*******************************************************************************
** Excel styler that does nothing - just takes defaults (which are all no-op) from the interface.
*******************************************************************************/
public class PlainExcelStyler implements ExcelStylerInterface
{
}

View File

@ -0,0 +1,119 @@
/*
* 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.scripts;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** Action to execute user/runtime defined code.
**
** This action is designed to support code in multiple languages, by using
** executors, e.g., provided by additional runtime qqq dependencies. Initially
** we are building qqq-language-support-javascript.
**
** We also have a Java executor, to provide at least a little bit of testability
** within qqq-backend-core. This executor is a candidate to be replaced in the
** future with something that would do actual dynamic java (whether that's compiled
** at runtime, or loaded from a plugin jar at runtime). In other words, the java
** executor in place today is just meant to be a placeholder.
*******************************************************************************/
public class ExecuteCodeAction
{
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public void run(ExecuteCodeInput input, ExecuteCodeOutput output) throws QException, QCodeException
{
QCodeReference codeReference = input.getCodeReference();
QCodeExecutionLoggerInterface executionLogger = input.getExecutionLogger();
if(executionLogger == null)
{
executionLogger = getDefaultExecutionLogger();
}
executionLogger.acceptExecutionStart(input);
try
{
String languageExecutor = switch(codeReference.getCodeType())
{
case JAVA -> "com.kingsrook.qqq.backend.core.actions.scripts.QJavaExecutor";
case JAVA_SCRIPT -> "com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor";
};
@SuppressWarnings("unchecked")
Class<? extends QCodeExecutor> executorClass = (Class<? extends QCodeExecutor>) Class.forName(languageExecutor);
QCodeExecutor qCodeExecutor = executorClass.getConstructor().newInstance();
////////////////////////////////////////////////////////////////////////////////////////////////////
// merge all of the input context, plus the input... input - into a context for the code executor //
////////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Serializable> context = new HashMap<>();
if(input.getContext() != null)
{
context.putAll(input.getContext());
}
if(input.getInput() != null)
{
context.putAll(input.getInput());
}
Serializable codeOutput = qCodeExecutor.execute(codeReference, context, executionLogger);
output.setOutput(codeOutput);
executionLogger.acceptExecutionEnd(codeOutput);
}
catch(QCodeException qCodeException)
{
executionLogger.acceptException(qCodeException);
throw (qCodeException);
}
catch(Exception e)
{
executionLogger.acceptException(e);
throw (new QException("Error executing code [" + codeReference + "]", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private QCodeExecutionLoggerInterface getDefaultExecutionLogger()
{
return (new Log4jCodeExecutionLogger());
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.scripts;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** Interface to be implemented by language-specific code executors, e.g., in
** qqq-language-support-${languageName} maven modules.
*******************************************************************************/
public interface QCodeExecutor
{
/*******************************************************************************
**
*******************************************************************************/
Serializable execute(QCodeReference codeReference, Map<String, Serializable> inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException;
}

View File

@ -0,0 +1,68 @@
/*
* 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.scripts;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** Java
*******************************************************************************/
public class QJavaExecutor implements QCodeExecutor
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public Serializable execute(QCodeReference codeReference, Map<String, Serializable> inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
{
Map<String, Object> context = new HashMap<>(inputContext);
if(!context.containsKey("logger"))
{
context.put("logger", executionLogger);
}
Serializable output;
try
{
Function<Map<String, Object>, Serializable> function = QCodeLoader.getFunction(codeReference);
output = function.apply(context);
}
catch(Exception e)
{
QCodeException qCodeException = new QCodeException("Error executing script", e);
throw (qCodeException);
}
return (output);
}
}

View File

@ -0,0 +1,191 @@
/*
* 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.scripts;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptOutput;
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.metadata.code.AssociatedScriptCodeReference;
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.scripts.Script;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
/*******************************************************************************
**
*******************************************************************************/
public class RunAssociatedScriptAction
{
private Map<AssociatedScriptCodeReference, ScriptRevision> scriptRevisionCache = new HashMap<>();
/*******************************************************************************
**
*******************************************************************************/
public void run(RunAssociatedScriptInput input, RunAssociatedScriptOutput output) throws QException
{
ActionHelper.validateSession(input);
ScriptRevision scriptRevision = getScriptRevision(input);
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
executeCodeInput.setInput(new HashMap<>(input.getInputValues()));
executeCodeInput.setContext(new HashMap<>());
if(input.getOutputObject() != null)
{
executeCodeInput.getContext().put("output", input.getOutputObject());
}
if(input.getScriptUtils() != null)
{
executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils());
}
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
/////////////////////////////////////////////////////////////////////////////////////////////////
// let caller supply a logger, or by default use StoreScriptLogAndScriptLogLineExecutionLogger //
/////////////////////////////////////////////////////////////////////////////////////////////////
QCodeExecutionLoggerInterface executionLogger = Objects.requireNonNullElseGet(input.getLogger(), () -> new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
executeCodeInput.setExecutionLogger(executionLogger);
if(executionLogger instanceof ScriptExecutionLoggerInterface scriptExecutionLoggerInterface)
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// if logger is aware of scripts (as opposed to a generic CodeExecution logger), give it the ids. //
////////////////////////////////////////////////////////////////////////////////////////////////////
scriptExecutionLoggerInterface.setScriptId(scriptRevision.getScriptId());
scriptExecutionLoggerInterface.setScriptRevisionId(scriptRevision.getId());
}
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
output.setOutput(executeCodeOutput.getOutput());
}
/*******************************************************************************
**
*******************************************************************************/
private ScriptRevision getScriptRevision(RunAssociatedScriptInput input) throws QException
{
if(!scriptRevisionCache.containsKey(input.getCodeReference()))
{
Serializable scriptId = getScriptId(input);
if(scriptId == null)
{
throw (new QNotFoundException("The input record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ "] does not have a script specified for [" + input.getCodeReference().getFieldName() + "]"));
}
Script script = getScript(input, scriptId);
if(script.getCurrentScriptRevisionId() == null)
{
throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ "] (scriptId=" + scriptId + ") does not have a current version."));
}
ScriptRevision scriptRevision = getCurrentScriptRevision(input, script.getCurrentScriptRevisionId());
scriptRevisionCache.put(input.getCodeReference(), scriptRevision);
}
return scriptRevisionCache.get(input.getCodeReference());
}
/*******************************************************************************
**
*******************************************************************************/
private ScriptRevision getCurrentScriptRevision(RunAssociatedScriptInput input, Serializable scriptRevisionId) throws QException
{
GetInput getInput = new GetInput();
getInput.setTableName("scriptRevision");
getInput.setPrimaryKey(scriptRevisionId);
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() == null)
{
throw (new QNotFoundException("The current revision of the script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "]["
+ input.getCodeReference().getFieldName() + "] (scriptRevisionId=" + scriptRevisionId + ") was not found."));
}
return (new ScriptRevision(getOutput.getRecord()));
}
/*******************************************************************************
**
*******************************************************************************/
private Script getScript(RunAssociatedScriptInput input, Serializable scriptId) throws QException
{
GetInput getInput = new GetInput();
getInput.setTableName("script");
getInput.setPrimaryKey(scriptId);
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() == null)
{
throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "]["
+ input.getCodeReference().getFieldName() + "] (script id=" + scriptId + ") was not found."));
}
return (new Script(getOutput.getRecord()));
}
/*******************************************************************************
**
*******************************************************************************/
private Serializable getScriptId(RunAssociatedScriptInput input) throws QException
{
GetInput getInput = new GetInput();
getInput.setTableName(input.getCodeReference().getRecordTable());
getInput.setPrimaryKey(input.getCodeReference().getRecordPrimaryKey());
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() == null)
{
throw (new QNotFoundException("The requested record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "] was not found."));
}
return (getOutput.getRecord().getValue(input.getCodeReference().getFieldName()));
}
}

View File

@ -0,0 +1,219 @@
/*
* 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.scripts;
import java.io.Serializable;
import java.util.List;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput;
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.QCriteriaOperator;
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;
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.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Action to store a new version of a script, associated with a record.
**
** If there's never been a script assigned to the record (for the specified field),
** then a new Script record is first inserted.
**
** The script referenced by the record is always updated to point at the new
** scriptRevision record that is inserted.
**
*******************************************************************************/
public class StoreAssociatedScriptAction
{
/*******************************************************************************
**
*******************************************************************************/
public void run(StoreAssociatedScriptInput input, StoreAssociatedScriptOutput output) throws QException
{
ActionHelper.validateSession(input);
QTableMetaData table = input.getTable();
Optional<AssociatedScript> optAssociatedScript = table.getAssociatedScripts().stream().filter(as -> as.getFieldName().equals(input.getFieldName())).findFirst();
if(optAssociatedScript.isEmpty())
{
throw (new QException("Field to update associated script for is not an associated script field."));
}
AssociatedScript associatedScript = optAssociatedScript.get();
/////////////////////////////////////////////////////////////
// get the record that the script is to be associated with //
/////////////////////////////////////////////////////////////
QRecord associatedRecord;
{
GetInput getInput = new GetInput();
getInput.setTableName(input.getTableName());
getInput.setPrimaryKey(input.getRecordPrimaryKey());
getInput.setShouldGenerateDisplayValues(true);
GetOutput getOutput = new GetAction().execute(getInput);
associatedRecord = getOutput.getRecord();
}
if(associatedRecord == null)
{
throw (new QException("Record to associated with script was not found."));
}
//////////////////////////////////////////////////////////////////
// check if there's currently a script referenced by the record //
//////////////////////////////////////////////////////////////////
Serializable existingScriptId = associatedRecord.getValueString(input.getFieldName());
QRecord script;
Integer nextSequenceNo = 1;
if(existingScriptId == null)
{
////////////////////////////////////////////////////////////////////
// get the script type - that'll be part of the new script's name //
////////////////////////////////////////////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName("scriptType");
getInput.setPrimaryKey(associatedScript.getScriptTypeId());
getInput.setShouldGenerateDisplayValues(true);
GetOutput getOutput = new GetAction().execute(getInput);
QRecord scriptType = getOutput.getRecord();
if(scriptType == null)
{
throw (new QException("Script type [" + associatedScript.getScriptTypeId() + "] was not found."));
}
/////////////////////////
// insert a new script //
/////////////////////////
script = new QRecord();
script.setValue("scriptTypeId", associatedScript.getScriptTypeId());
script.setValue("name", associatedRecord.getRecordLabel() + " - " + scriptType.getRecordLabel());
InsertInput insertInput = new InsertInput();
insertInput.setTableName("script");
insertInput.setRecords(List.of(script));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
script = insertOutput.getRecords().get(0);
/////////////////////////////////////////////////////////////
// update the associated record to point at the new script //
/////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(input.getTableName());
updateInput.setRecords(List.of(new QRecord()
.withValue(table.getPrimaryKeyField(), associatedRecord.getValue(table.getPrimaryKeyField()))
.withValue(input.getFieldName(), script.getValue("id"))
));
new UpdateAction().execute(updateInput);
}
else
{
////////////////////////////////////////
// get the existing script, to update //
////////////////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName("script");
getInput.setPrimaryKey(existingScriptId);
GetOutput getOutput = new GetAction().execute(getInput);
script = getOutput.getRecord();
QueryInput queryInput = new QueryInput();
queryInput.setTableName("scriptRevision");
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
);
queryInput.setLimit(1);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(!queryOutput.getRecords().isEmpty())
{
nextSequenceNo = queryOutput.getRecords().get(0).getValueInteger("sequenceNo") + 1;
}
}
//////////////////////////////////
// insert a new script revision //
//////////////////////////////////
String commitMessage = input.getCommitMessage();
if(!StringUtils.hasContent(commitMessage))
{
if(nextSequenceNo == 1)
{
commitMessage = "Initial version";
}
else
{
commitMessage = "No commit message given";
}
}
QRecord scriptRevision = new QRecord()
.withValue("scriptId", script.getValue("id"))
.withValue("contents", input.getCode())
.withValue("commitMessage", commitMessage)
.withValue("sequenceNo", nextSequenceNo);
try
{
scriptRevision.setValue("author", input.getSession().getUser().getFullName());
}
catch(Exception e)
{
scriptRevision.setValue("author", "Unknown");
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptRevision");
insertInput.setRecords(List.of(scriptRevision));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
scriptRevision = insertOutput.getRecords().get(0);
////////////////////////////////////////////////////
// update the script to point at the new revision //
////////////////////////////////////////////////////
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName("script");
updateInput.setRecords(List.of(script));
new UpdateAction().execute(updateInput);
output.setScriptId(script.getValueInteger("id"));
output.setScriptName(script.getValueString("name"));
output.setScriptRevisionId(scriptRevision.getValueInteger("id"));
output.setScriptRevisionSequenceNo(scriptRevision.getValueInteger("sequenceNo"));
}
}

View File

@ -0,0 +1,108 @@
/*
* 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.scripts;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
/*******************************************************************************
** Interface to be implemented by script-running actions, if they want to allow
** themselves to be used for user-testing of their script.
*******************************************************************************/
public interface TestScriptActionInterface
{
/*******************************************************************************
** Called to adapt or translate data from the TestScriptInput (which would just
** have a map of name-value pairs) to the actual input object(s) used by the script.
**
** Note - such a method may want or need to put an "output" object into the
** executeCodeInput's context map.
*******************************************************************************/
void setupTestScriptInput(TestScriptInput testScriptInput, ExecuteCodeInput executeCodeInput);
/*******************************************************************************
** Called to adapt or translate the output object of the script execution to
** something suitable for returning to the caller.
**
** Default implementation may always be suitable?
*******************************************************************************/
default Serializable processTestScriptOutput(ExecuteCodeOutput executeCodeOutput)
{
return (executeCodeOutput.getOutput());
}
/*******************************************************************************
** Define the list of input fields for testing the script. The names of these
** fields will end up as keys in the setupTestScriptInput method's testScriptInput object.
*******************************************************************************/
List<QFieldMetaData> getTestInputFields();
/*******************************************************************************
** Define the list of output fields when testing the script. The output object
** returned from processTestScriptOutput should have keys that match these field names.
*******************************************************************************/
List<QFieldMetaData> getTestOutputFields();
/*******************************************************************************
** Execute a test script.
*******************************************************************************/
default void execute(TestScriptInput input, TestScriptOutput output) throws QException
{
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
executeCodeInput.setContext(new HashMap<>());
executeCodeInput.setCodeReference(input.getCodeReference());
BuildScriptLogAndScriptLogLineExecutionLogger executionLogger = new BuildScriptLogAndScriptLogLineExecutionLogger(null, null);
executeCodeInput.setExecutionLogger(executionLogger);
setupTestScriptInput(input, executeCodeInput);
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
try
{
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
output.setOutputObject(processTestScriptOutput(executeCodeOutput));
}
catch(Exception e)
{
output.setException(e);
}
output.setScriptLog(executionLogger.getScriptLog());
output.setScriptLogLines(executionLogger.getScriptLogLines());
}
}

View File

@ -0,0 +1,160 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
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;
/*******************************************************************************
**
*******************************************************************************/
public class AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLogAndScriptLogLineExecutionLogger implements ScriptExecutionLoggerInterface
{
private static final QLogger LOG = QLogger.getLogger(AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger.class);
private List<QRecord> scriptLogs = new ArrayList<>();
private List<List<QRecord>> scriptLogLines = new ArrayList<>();
/*******************************************************************************
**
*******************************************************************************/
public AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger()
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
{
super.acceptExecutionStart(executeCodeInput);
super.setScriptLogLines(new ArrayList<>());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptException(Exception exception)
{
accumulate(null, exception);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionEnd(Serializable output)
{
accumulate(output, null);
}
/*******************************************************************************
**
*******************************************************************************/
private void accumulate(Serializable output, Exception exception)
{
super.updateHeaderAtEnd(output, exception);
scriptLogs.add(super.getScriptLog());
scriptLogLines.add(new ArrayList<>(super.getScriptLogLines()));
super.getScriptLogLines().clear();
}
/*******************************************************************************
**
*******************************************************************************/
public void storeAndClear()
{
try
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptLog");
insertInput.setRecords(scriptLogs);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
List<QRecord> flatScriptLogLines = new ArrayList<>();
for(int i = 0; i < insertOutput.getRecords().size(); i++)
{
QRecord insertedScriptLog = insertOutput.getRecords().get(i);
List<QRecord> subScriptLogLines = scriptLogLines.get(i);
subScriptLogLines.forEach(r -> r.setValue("scriptLogId", insertedScriptLog.getValueInteger("id")));
flatScriptLogLines.addAll(subScriptLogLines);
}
insertInput = new InsertInput();
insertInput.setTableName("scriptLogLine");
insertInput.setRecords(flatScriptLogLines);
new InsertAction().execute(insertInput);
}
catch(Exception e)
{
LOG.warn("Error storing script logs", e);
}
scriptLogs.clear();
scriptLogLines.clear();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void setScriptId(Integer scriptId)
{
super.setScriptId(scriptId);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void setScriptRevisionId(Integer scriptRevisionId)
{
super.setScriptRevisionId(scriptRevisionId);
}
}

View File

@ -0,0 +1,253 @@
/*
* 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.scripts.logging;
import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Implementation of a code execution logger that builds a scriptLog and 0 or more
** scriptLogLine records - but doesn't insert them. e.g., useful for testing
** (both in junit, and for users in-app).
*******************************************************************************/
public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecutionLoggerInterface, ScriptExecutionLoggerInterface
{
private static final QLogger LOG = QLogger.getLogger(BuildScriptLogAndScriptLogLineExecutionLogger.class);
private QRecord scriptLog;
private List<QRecord> scriptLogLines = new ArrayList<>();
protected ExecuteCodeInput executeCodeInput;
private Serializable scriptId;
private Serializable scriptRevisionId;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BuildScriptLogAndScriptLogLineExecutionLogger()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BuildScriptLogAndScriptLogLineExecutionLogger(Serializable scriptId, Serializable scriptRevisionId)
{
this.scriptId = scriptId;
this.scriptRevisionId = scriptRevisionId;
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord buildHeaderRecord(ExecuteCodeInput executeCodeInput)
{
return (new QRecord()
.withValue("scriptId", scriptId)
.withValue("scriptRevisionId", scriptRevisionId)
.withValue("startTimestamp", Instant.now())
.withValue("input", truncate(executeCodeInput.getInput())));
}
/*******************************************************************************
**
*******************************************************************************/
protected QRecord buildDetailLogRecord(String logLine)
{
return (new QRecord()
.withValue("scriptLogId", scriptLog.getValue("id"))
.withValue("timestamp", Instant.now())
.withValue("text", truncate(logLine)));
}
/*******************************************************************************
**
*******************************************************************************/
private String truncate(Object o)
{
return StringUtils.safeTruncate(ValueUtils.getValueAsString(o), 1000, "...");
}
/*******************************************************************************
**
*******************************************************************************/
protected void updateHeaderAtEnd(Serializable output, Exception exception)
{
Instant startTimestamp = (Instant) scriptLog.getValue("startTimestamp");
Instant endTimestamp = Instant.now();
scriptLog.setValue("endTimestamp", endTimestamp);
scriptLog.setValue("runTimeMillis", startTimestamp.until(endTimestamp, ChronoUnit.MILLIS));
if(exception != null)
{
scriptLog.setValue("hadError", true);
scriptLog.setValue("error", exception.getMessage());
}
else
{
scriptLog.setValue("hadError", false);
scriptLog.setValue("output", truncate(output));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
{
try
{
this.executeCodeInput = executeCodeInput;
this.scriptLog = buildHeaderRecord(executeCodeInput);
}
catch(Exception e)
{
LOG.warn("Error starting storage of script log", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptLogLine(String logLine)
{
scriptLogLines.add(buildDetailLogRecord(logLine));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptException(Exception exception)
{
updateHeaderAtEnd(null, exception);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionEnd(Serializable output)
{
updateHeaderAtEnd(output, null);
}
/*******************************************************************************
** Getter for scriptLog
**
*******************************************************************************/
public QRecord getScriptLog()
{
return scriptLog;
}
/*******************************************************************************
** Getter for scriptLogLines
**
*******************************************************************************/
public List<QRecord> getScriptLogLines()
{
return scriptLogLines;
}
/*******************************************************************************
** Setter for scriptLog
**
*******************************************************************************/
protected void setScriptLog(QRecord scriptLog)
{
this.scriptLog = scriptLog;
}
/*******************************************************************************
** Setter for scriptLogLines
**
*******************************************************************************/
protected void setScriptLogLines(List<QRecord> scriptLogLines)
{
this.scriptLogLines = scriptLogLines;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void setScriptId(Integer scriptId)
{
this.scriptId = scriptId;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void setScriptRevisionId(Integer scriptRevisionId)
{
this.scriptRevisionId = scriptRevisionId;
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.scripts.logging;
import java.io.Serializable;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Implementation of a code execution logger that logs to LOG 4j
*******************************************************************************/
public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
{
private static final QLogger LOG = QLogger.getLogger(Log4jCodeExecutionLogger.class);
private QCodeReference qCodeReference;
private String uuid = UUID.randomUUID().toString();
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
{
this.qCodeReference = executeCodeInput.getCodeReference();
String inputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(executeCodeInput.getInput()), 250, "...");
LOG.info("Starting script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with input: " + inputString);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptLogLine(String logLine)
{
LOG.info("Script log: " + uuid + ": " + logLine);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptException(Exception exception)
{
LOG.info("Script Exception: " + uuid, exception);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionEnd(Serializable output)
{
String outputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(output), 250, "...");
LOG.info("Finished script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with output: " + outputString);
}
}

View File

@ -19,61 +19,56 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.modules.authentication.metadata;
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
/*******************************************************************************
** Meta-data to provide details of an Auth0 Authentication module
** Implementation of a code execution logger that just noop's every action.
*******************************************************************************/
public class Auth0AuthenticationMetaData extends QAuthenticationMetaData
public class NoopCodeExecutionLogger implements QCodeExecutionLoggerInterface
{
private String baseUrl;
/*******************************************************************************
** Default Constructor.
*******************************************************************************/
public Auth0AuthenticationMetaData()
{
super();
setType(QAuthenticationType.AUTH_0);
}
/*******************************************************************************
** Fluent setter, override to help fluent flows
*******************************************************************************/
public Auth0AuthenticationMetaData withBaseUrl(String baseUrl)
{
setBaseUrl(baseUrl);
return this;
}
/*******************************************************************************
** Getter for baseUrl
**
*******************************************************************************/
public String getBaseUrl()
@Override
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
{
return baseUrl;
}
/*******************************************************************************
** Setter for baseUrl
**
*******************************************************************************/
public void setBaseUrl(String baseUrl)
@Override
public void acceptLogLine(String logLine)
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptException(Exception exception)
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionEnd(Serializable output)
{
this.baseUrl = baseUrl;
}
}

View File

@ -0,0 +1,64 @@
/*
* 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.scripts.logging;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
/*******************************************************************************
** Interface to provide logging functionality to QCodeExecution (e.g., scripts)
*******************************************************************************/
public interface QCodeExecutionLoggerInterface
{
/*******************************************************************************
** Called when the execution starts - takes the execution's input object.
*******************************************************************************/
void acceptExecutionStart(ExecuteCodeInput executeCodeInput);
/*******************************************************************************
** Called to log a line, a message.
*******************************************************************************/
void acceptLogLine(String logLine);
/*******************************************************************************
** In case the loggerInterface object is provided to the script as context,
** this method gives a clean interface for the script to log a line.
*******************************************************************************/
default void log(String message)
{
acceptLogLine(message);
}
/*******************************************************************************
** Called if the script fails with an exception.
*******************************************************************************/
void acceptException(Exception exception);
/*******************************************************************************
** Called if the script completes without exception.
*******************************************************************************/
void acceptExecutionEnd(Serializable output);
}

View File

@ -0,0 +1,39 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
/*******************************************************************************
**
*******************************************************************************/
public interface ScriptExecutionLoggerInterface
{
/*******************************************************************************
**
*******************************************************************************/
void setScriptId(Integer scriptId);
/*******************************************************************************
**
*******************************************************************************/
void setScriptRevisionId(Integer scriptRevisionId);
}

View File

@ -0,0 +1,132 @@
/*
* 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.scripts.logging;
import java.io.Serializable;
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.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
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.update.UpdateInput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Implementation of a code execution logger that logs into scriptLog and scriptLogLine
** tables - e.g., as defined in ScriptMetaDataProvider.
*******************************************************************************/
public class StoreScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLogAndScriptLogLineExecutionLogger
{
private static final QLogger LOG = QLogger.getLogger(StoreScriptLogAndScriptLogLineExecutionLogger.class);
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public StoreScriptLogAndScriptLogLineExecutionLogger(Serializable scriptId, Serializable scriptRevisionId)
{
super(scriptId, scriptRevisionId);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
{
try
{
super.acceptExecutionStart(executeCodeInput);
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptLog");
insertInput.setRecords(List.of(getScriptLog()));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
setScriptLog(insertOutput.getRecords().get(0));
}
catch(Exception e)
{
LOG.warn("Error starting storage of script log", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptException(Exception exception)
{
store(null, exception);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionEnd(Serializable output)
{
store(output, null);
}
/*******************************************************************************
**
*******************************************************************************/
private void store(Serializable output, Exception exception)
{
try
{
updateHeaderAtEnd(output, exception);
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName("scriptLog");
updateInput.setRecords(List.of(getScriptLog()));
new UpdateAction().execute(updateInput);
if(CollectionUtils.nullSafeHasContents(getScriptLogLines()))
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptLogLine");
insertInput.setRecords(getScriptLogLines());
new InsertAction().execute(insertInput);
}
}
catch(Exception e)
{
LOG.warn("Error storing script log", e);
}
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.tables;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
/*******************************************************************************
** Action to run an aggregate against a table.
**
*******************************************************************************/
public class AggregateAction
{
/*******************************************************************************
**
*******************************************************************************/
public AggregateOutput execute(AggregateInput aggregateInput) throws QException
{
ActionHelper.validateSession(aggregateInput);
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(aggregateInput.getBackend());
// todo pre-customization - just get to modify the request?
AggregateOutput aggregateOutput = qModule.getAggregateInterface().execute(aggregateInput);
// todo post-customization - can do whatever w/ the result if you want
return aggregateOutput;
}
}

View File

@ -28,6 +28,7 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
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.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
@ -35,8 +36,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -45,7 +44,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class DeleteAction
{
private static final Logger LOG = LogManager.getLogger(DeleteAction.class);
private static final QLogger LOG = QLogger.getLogger(DeleteAction.class);
@ -102,7 +101,7 @@ public class DeleteAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
QueryInput queryInput = new QueryInput(deleteInput.getInstance(), deleteInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(deleteInput.getTableName());
queryInput.setFilter(deleteInput.getQueryFilter());
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);

View File

@ -0,0 +1,351 @@
/*
* 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.tables;
import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
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.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.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
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 org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
** Action to run a get against a table.
**
*******************************************************************************/
public class GetAction
{
private Optional<AbstractPostQueryCustomizer> postGetRecordCustomizer;
private GetInput getInput;
private QPossibleValueTranslator qPossibleValueTranslator;
/*******************************************************************************
**
*******************************************************************************/
public GetOutput execute(GetInput getInput) throws QException
{
ActionHelper.validateSession(getInput);
QTableMetaData table = getInput.getTable();
if(table == null)
{
throw (new QException("Requested to Get a record from an unrecognized table: " + getInput.getTableName()));
}
postGetRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
this.getInput = getInput;
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(getInput.getBackend());
// todo pre-customization - just get to modify the request?
GetInterface getInterface = null;
try
{
getInterface = qModule.getGetInterface();
}
catch(IllegalStateException ise)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if a module doesn't implement Get directly - try to do a Get by a Query in the DefaultGetInterface (inner class) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
GetOutput getOutput;
if(getInterface == null)
{
getInterface = new DefaultGetInterface();
}
getInterface.validateInput(getInput);
getOutput = getInterface.execute(getInput);
////////////////////////////
// handle cache use-cases //
////////////////////////////
if(table.getCacheOf() != null)
{
if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////////////////////
// if the record wasn't found, see if we should look in cache-source //
///////////////////////////////////////////////////////////////////////
QRecord recordFromSource = tryToGetFromCacheSource(getInput, getOutput);
if(recordFromSource != null)
{
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(getInput.getTableName());
insertInput.setRecords(List.of(recordToCache));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
getOutput.setRecord(insertOutput.getRecords().get(0));
}
}
else
{
/////////////////////////////////////////////////////////////////////////////////
// if the record was found, but it's too old, maybe re-fetch from cache source //
/////////////////////////////////////////////////////////////////////////////////
refreshCacheIfExpired(getInput, getOutput);
}
}
////////////////////////////////////////////////////////
// if the record is found, perform post-actions on it //
////////////////////////////////////////////////////////
if(getOutput.getRecord() != null)
{
getOutput.setRecord(postRecordActions(getOutput.getRecord()));
}
return getOutput;
}
/*******************************************************************************
**
*******************************************************************************/
private static QRecord mapSourceRecordToCacheRecord(QTableMetaData table, QRecord recordFromSource)
{
QRecord cacheRecord = new QRecord(recordFromSource);
if(StringUtils.hasContent(table.getCacheOf().getCachedDateFieldName()))
{
cacheRecord.setValue(table.getCacheOf().getCachedDateFieldName(), Instant.now());
}
return (cacheRecord);
}
/*******************************************************************************
**
*******************************************************************************/
private void refreshCacheIfExpired(GetInput getInput, GetOutput getOutput) throws QException
{
QTableMetaData table = getInput.getTable();
Integer expirationSeconds = table.getCacheOf().getExpirationSeconds();
if(expirationSeconds != null)
{
QRecord cachedRecord = getOutput.getRecord();
Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName());
if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS)))
{
QRecord recordFromSource = tryToGetFromCacheSource(getInput, getOutput);
if(recordFromSource != null)
{
///////////////////////////////////////////////////////////////////
// if the record was found in the source, update it in the cache //
///////////////////////////////////////////////////////////////////
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
recordToCache.setValue(table.getPrimaryKeyField(), cachedRecord.getValue(table.getPrimaryKeyField()));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(getInput.getTableName());
updateInput.setRecords(List.of(recordToCache));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
getOutput.setRecord(updateOutput.getRecords().get(0));
}
else
{
/////////////////////////////////////////////////////////////////////////////
// if the record is no longer in the source, then remove it from the cache //
/////////////////////////////////////////////////////////////////////////////
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(getInput.getTableName());
deleteInput.setPrimaryKeys(List.of(getOutput.getRecord().getValue(table.getPrimaryKeyField())));
new DeleteAction().execute(deleteInput);
getOutput.setRecord(null);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord tryToGetFromCacheSource(GetInput getInput, GetOutput getOutput) throws QException
{
QRecord recordFromSource = null;
QTableMetaData table = getInput.getTable();
for(CacheUseCase cacheUseCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
if(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY.equals(cacheUseCase.getType()) && getInput.getUniqueKey() != null)
{
recordFromSource = getFromCachedSourceForUniqueKeyToUniqueKey(getInput, table.getCacheOf().getSourceTable());
break;
}
else
{
// todo!!
throw new NotImplementedException("Not-yet-implemented cache use case type: " + cacheUseCase.getType());
}
}
return (recordFromSource);
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord getFromCachedSourceForUniqueKeyToUniqueKey(GetInput getInput, String sourceTableName) throws QException
{
/////////////////////////////////////////////////////
// do a Get on the source table, by the unique key //
/////////////////////////////////////////////////////
GetInput sourceGetInput = new GetInput();
sourceGetInput.setTableName(sourceTableName);
sourceGetInput.setUniqueKey(getInput.getUniqueKey());
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
return (sourceGetOutput.getRecord());
}
/*******************************************************************************
**
*******************************************************************************/
private static class DefaultGetInterface implements GetInterface
{
@Override
public GetOutput execute(GetInput getInput) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(getInput.getTableName());
//////////////////////////////////////////////////
// build filter using either pkey or unique key //
//////////////////////////////////////////////////
QQueryFilter filter = new QQueryFilter();
if(getInput.getPrimaryKey() != null)
{
filter.addCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey()));
}
else if(getInput.getUniqueKey() != null)
{
for(Map.Entry<String, Serializable> entry : getInput.getUniqueKey().entrySet())
{
if(entry.getValue() == null)
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.IS_BLANK));
}
else
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue()));
}
}
}
else
{
throw (new QException("No primaryKey or uniqueKey was passed to Get"));
}
queryInput.setFilter(filter);
queryInput.setShouldFetchHeavyFields(getInput.getShouldFetchHeavyFields());
QueryOutput queryOutput = new QueryAction().execute(queryInput);
GetOutput getOutput = new GetOutput();
if(!queryOutput.getRecords().isEmpty())
{
getOutput.setRecord(queryOutput.getRecords().get(0));
}
return (getOutput);
}
}
/*******************************************************************************
** Run the necessary actions on a record. This may include setting display values,
** translating possible values, and running post-record customizations.
*******************************************************************************/
public QRecord postRecordActions(QRecord record)
{
QRecord returnRecord = record;
if(this.postGetRecordCustomizer.isPresent())
{
returnRecord = postGetRecordCustomizer.get().apply(List.of(record)).get(0);
}
if(getInput.getShouldTranslatePossibleValues())
{
if(qPossibleValueTranslator == null)
{
qPossibleValueTranslator = new QPossibleValueTranslator(getInput.getInstance(), getInput.getSession());
}
qPossibleValueTranslator.translatePossibleValuesInRecords(getInput.getTable(), List.of(returnRecord));
}
if(getInput.getShouldGenerateDisplayValues())
{
QValueFormatter.setDisplayValuesInRecords(getInput.getTable(), List.of(returnRecord));
}
//////////////////////////////////////////////////////////////////////////////
// note - shouldFetchHeavyFields should be handled by the underlying action //
//////////////////////////////////////////////////////////////////////////////
return (returnRecord);
}
}

View File

@ -22,41 +22,152 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostInsertCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
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.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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Action to insert one or more records.
**
*******************************************************************************/
public class InsertAction
public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOutput>
{
private static final Logger LOG = LogManager.getLogger(InsertAction.class);
private static final QLogger LOG = QLogger.getLogger(InsertAction.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public InsertOutput execute(InsertInput insertInput) throws QException
{
ActionHelper.validateSession(insertInput);
QTableMetaData table = insertInput.getTable();
Optional<AbstractPostInsertCustomizer> postInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPostInsertCustomizer.class, table, TableCustomizers.POST_INSERT_RECORD.getRole());
setAutomationStatusField(insertInput);
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
// todo - need to handle records with errors coming out of here...
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
// todo pre-customization - just get to modify the request?
setErrorsIfUniqueKeyErrors(insertInput, table);
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
// todo post-customization - can do whatever w/ the result if you want
if(postInsertCustomizer.isPresent())
{
postInsertCustomizer.get().setInsertInput(insertInput);
insertOutput.setRecords(postInsertCustomizer.get().apply(insertOutput.getRecords()));
}
return insertOutput;
}
/*******************************************************************************
**
*******************************************************************************/
private void setErrorsIfUniqueKeyErrors(InsertInput insertInput, QTableMetaData table) throws QException
{
if(CollectionUtils.nullSafeHasContents(table.getUniqueKeys()))
{
Map<UniqueKey, Set<List<Serializable>>> keysInThisList = new HashMap<>();
if(insertInput.getSkipUniqueKeyCheck())
{
LOG.debug("Skipping unique key check in " + insertInput.getTableName() + " insert.");
return;
}
////////////////////////////////////////////
// check for any pre-existing unique keys //
////////////////////////////////////////////
Map<UniqueKey, Set<List<Serializable>>> existingKeys = new HashMap<>();
List<UniqueKey> uniqueKeys = CollectionUtils.nonNullList(table.getUniqueKeys());
for(UniqueKey uniqueKey : uniqueKeys)
{
existingKeys.put(uniqueKey, UniqueKeyHelper.getExistingKeys(insertInput, insertInput.getTransaction(), table, insertInput.getRecords(), uniqueKey));
}
/////////////////////////////////////
// make sure this map is populated //
/////////////////////////////////////
uniqueKeys.forEach(uk -> keysInThisList.computeIfAbsent(uk, x -> new HashSet<>()));
for(QRecord record : insertInput.getRecords())
{
//////////////////////////////////////////////////////////
// check if this record violates any of the unique keys //
//////////////////////////////////////////////////////////
boolean foundDupe = false;
for(UniqueKey uniqueKey : uniqueKeys)
{
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
if(keyValues.isPresent() && (existingKeys.get(uniqueKey).contains(keyValues.get()) || keysInThisList.get(uniqueKey).contains(keyValues.get())))
{
record.addError("Another record already exists with this " + uniqueKey.getDescription(table));
foundDupe = true;
break;
}
}
///////////////////////////////////////////////////////////////////////////////
// if this record doesn't violate any uk's, then we can add it to the output //
///////////////////////////////////////////////////////////////////////////////
if(!foundDupe)
{
for(UniqueKey uniqueKey : uniqueKeys)
{
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
keyValues.ifPresent(kv -> keysInThisList.get(uniqueKey).add(kv));
}
}
}
}
}
/*******************************************************************************
** If the table being inserted into uses an automation-status field, populate it now.
*******************************************************************************/
private void setAutomationStatusField(InsertInput insertInput)
{
RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getSession(), insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -24,13 +24,15 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
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.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -44,10 +46,11 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class QueryAction
{
private Optional<Function<QRecord, QRecord>> postQueryRecordCustomizer;
private static final QLogger LOG = QLogger.getLogger(QueryAction.class);
private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer;
private QueryInput queryInput;
private QValueFormatter qValueFormatter;
private QPossibleValueTranslator qPossibleValueTranslator;
@ -59,7 +62,7 @@ public class QueryAction
{
ActionHelper.validateSession(queryInput);
postQueryRecordCustomizer = QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
this.queryInput = queryInput;
if(queryInput.getRecordPipe() != null)
@ -73,6 +76,11 @@ public class QueryAction
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);
// todo post-customization - can do whatever w/ the result if you want
if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe)
{
bufferedRecordPipe.finalFlush();
}
if(queryInput.getRecordPipe() == null)
{
postRecordActions(queryOutput.getRecords());
@ -92,16 +100,7 @@ public class QueryAction
{
if(this.postQueryRecordCustomizer.isPresent())
{
records.replaceAll(t -> postQueryRecordCustomizer.get().apply(t));
}
if(queryInput.getShouldGenerateDisplayValues())
{
if(qValueFormatter == null)
{
qValueFormatter = new QValueFormatter();
}
qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records);
records = postQueryRecordCustomizer.get().apply(records);
}
if(queryInput.getShouldTranslatePossibleValues())
@ -110,7 +109,12 @@ public class QueryAction
{
qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession());
}
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records);
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records, queryInput.getQueryJoins(), queryInput.getFieldsToTranslatePossibleValues());
}
if(queryInput.getShouldGenerateDisplayValues())
{
QValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records);
}
}
}

View File

@ -23,6 +23,9 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
@ -42,6 +45,10 @@ public class UpdateAction
public UpdateOutput execute(UpdateInput updateInput) throws QException
{
ActionHelper.validateSession(updateInput);
setAutomationStatusField(updateInput);
ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), updateInput.getTable(), updateInput.getRecords());
// todo - need to handle records with errors coming out of here...
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend());
@ -50,4 +57,15 @@ public class UpdateAction
// todo post-customization - can do whatever w/ the result if you want
return updateResult;
}
/*******************************************************************************
** If the table being updated uses an automation-status field, populate it now.
*******************************************************************************/
private void setAutomationStatusField(UpdateInput updateInput)
{
RecordAutomationStatusUpdater.setAutomationStatusInRecords(updateInput.getSession(), updateInput.getTable(), updateInput.getRecords(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS);
}
}

View File

@ -0,0 +1,170 @@
/*
* 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.tables.helpers;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Methods to help with unique key checks.
*******************************************************************************/
public class UniqueKeyHelper
{
/*******************************************************************************
**
*******************************************************************************/
public static Set<List<Serializable>> getExistingKeys(AbstractActionInput actionInput, QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey) throws QException
{
List<String> ukFieldNames = uniqueKey.getFieldNames();
Set<List<Serializable>> existingRecords = new HashSet<>();
if(ukFieldNames != null)
{
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)
{
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
{
continue;
}
QQueryFilter subFilter = new QQueryFilter();
filter.addSubFilter(subFilter);
for(String fieldName : ukFieldNames)
{
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 - 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);
keyValues.ifPresent(existingRecords::add);
}
}
return (existingRecords);
}
/*******************************************************************************
**
*******************************************************************************/
public static Optional<List<Serializable>> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record)
{
try
{
List<Serializable> keyValues = new ArrayList<>();
for(String fieldName : uniqueKey.getFieldNames())
{
QFieldMetaData field = table.getField(fieldName);
Serializable value = record.getValue(fieldName);
Serializable typedValue = ValueUtils.getValueAsFieldType(field.getType(), value);
keyValues.add(typedValue == null ? new NullUniqueKeyValue() : typedValue);
}
return (Optional.of(keyValues));
}
catch(Exception e)
{
return (Optional.empty());
}
}
/*******************************************************************************
** To make a list of unique key values here behave like they do in an RDBMS
** (which is what we're trying to mimic - which is - 2 null values in a field
** aren't considered the same, so they don't violate a unique key) (at least, that's
** how some RDBMS's work, right??) - use this value instead of nulls in the
** output of getKeyValues - where interestingly, this class always returns
** false in it equals method... Unclear how bad this is, e.g., if it's violating
** the contract for equals and hashCode...
*******************************************************************************/
public static class NullUniqueKeyValue implements Serializable
{
@Override
public boolean equals(Object obj)
{
return (false);
}
}
}

View File

@ -0,0 +1,99 @@
/*
* 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.templates;
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.model.templates.ConvertHtmlToPdfInput;
import com.kingsrook.qqq.backend.core.model.templates.ConvertHtmlToPdfOutput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.jsoup.Jsoup;
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
*******************************************************************************/
public class ConvertHtmlToPdfAction extends AbstractQActionFunction<ConvertHtmlToPdfInput, ConvertHtmlToPdfOutput>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public ConvertHtmlToPdfOutput execute(ConvertHtmlToPdfInput input) throws QException
{
try
{
ConvertHtmlToPdfOutput output = new ConvertHtmlToPdfOutput();
//////////////////////////////////////////////////////////////////
// convert the input HTML to XHTML, as needed for ITextRenderer //
//////////////////////////////////////////////////////////////////
Document document = Jsoup.parse(input.getHtml());
document.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
//////////////////////////////
// convert the XHTML to PDF //
//////////////////////////////
ITextRenderer renderer = new ITextRenderer();
SharedContext sharedContext = renderer.getSharedContext();
sharedContext.setPrint(true);
sharedContext.setInteractive(false);
if(input.getBasePath() != null)
{
String baseUrl = input.getBasePath().toUri().toURL().toString();
renderer.setDocumentFromString(document.html(), baseUrl);
}
else
{
renderer.setDocumentFromString(document.html());
}
//////////////////////////////////////////////////
// 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);
}
renderer.layout();
renderer.createPDF(input.getOutputStream());
return (output);
}
catch(Exception e)
{
throw (new QException("Error converting html to pdf", e));
}
}
}

View File

@ -0,0 +1,93 @@
/*
* 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.templates;
import java.io.StringWriter;
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.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateInput;
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateOutput;
import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.context.Context;
/*******************************************************************************
** Basic action to render a template!
*******************************************************************************/
public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplateInput, RenderTemplateOutput>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderTemplateOutput execute(RenderTemplateInput input) throws QException
{
RenderTemplateOutput output = new RenderTemplateOutput();
if(TemplateType.VELOCITY.equals(input.getTemplateType()))
{
Velocity.init();
Context context = new VelocityContext(input.getContext());
StringWriter stringWriter = new StringWriter();
Velocity.evaluate(context, stringWriter, "logTag", input.getCode());
output.setResult(stringWriter.getBuffer().toString());
}
else
{
throw (new QException("Unsupported Template Type: " + input.getTemplateType()));
}
return (output);
}
/*******************************************************************************
** Most convenient static wrapper to render a Velocity template.
*******************************************************************************/
public static String renderVelocity(AbstractActionInput parentActionInput, Map<String, Object> context, String code) throws QException
{
return (render(parentActionInput, TemplateType.VELOCITY, context, code));
}
/*******************************************************************************
** Convenient static wrapper to render a template of an arbitrary type (language).
*******************************************************************************/
public static String render(AbstractActionInput parentActionInput, TemplateType templateType, Map<String, Object> context, String code) throws QException
{
RenderTemplateInput renderTemplateInput = new RenderTemplateInput();
renderTemplateInput.setCode(code);
renderTemplateInput.setContext(context);
renderTemplateInput.setTemplateType(templateType);
RenderTemplateOutput output = new RenderTemplateAction().execute(renderTemplateInput);
return (output.getResult());
}
}

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.values;
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;
@ -33,14 +34,18 @@ import java.util.Objects;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.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.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
@ -48,8 +53,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@ -58,7 +64,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QPossibleValueTranslator
{
private static final Logger LOG = LogManager.getLogger(QPossibleValueTranslator.class);
private static final QLogger LOG = QLogger.getLogger(QPossibleValueTranslator.class);
private final QInstance qInstance;
private final QSession session;
@ -69,6 +75,8 @@ public class QPossibleValueTranslator
///////////////////////////////////////////////////////
private Map<String, Map<Serializable, String>> possibleValueCache;
// todo not commit - remove instance & session - use Context
/*******************************************************************************
@ -89,31 +97,119 @@ public class QPossibleValueTranslator
*******************************************************************************/
public void translatePossibleValuesInRecords(QTableMetaData table, List<QRecord> records)
{
if(records == null)
translatePossibleValuesInRecords(table, records, Collections.emptyList(), null);
}
/*******************************************************************************
** For a list of records, translate their possible values (populating their display values)
*******************************************************************************/
public void translatePossibleValuesInRecords(QTableMetaData table, List<QRecord> records, List<QueryJoin> queryJoins, Set<String> limitedToFieldNames)
{
if(records == null || table == null)
{
return;
}
primePvsCache(table, records);
if(limitedToFieldNames != null && limitedToFieldNames.isEmpty())
{
LOG.debug("We were asked to translate possible values, but then given an empty set of fields to translate, so noop.");
return;
}
LOG.debug("Translating possible values in [" + records.size() + "] records from the [" + table.getName() + "] table.");
primePvsCache(table, records, queryJoins, limitedToFieldNames);
for(QRecord record : records)
{
for(QFieldMetaData field : table.getFields().values())
{
if(field.getPossibleValueSourceName() != null)
{
if(limitedToFieldNames == null || limitedToFieldNames.contains(field.getName()))
{
record.setDisplayValue(field.getName(), translatePossibleValue(field, record.getValue(field.getName())));
}
}
}
for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins))
{
if(queryJoin.getSelect())
{
try
{
QTableMetaData joinTable = qInstance.getTable(queryJoin.getJoinTable());
for(QFieldMetaData field : joinTable.getFields().values())
{
String joinFieldName = Objects.requireNonNullElse(queryJoin.getAlias(), joinTable.getName()) + "." + field.getName();
if(field.getPossibleValueSourceName() != null)
{
if(limitedToFieldNames == null || limitedToFieldNames.contains(joinFieldName))
{
///////////////////////////////////////////////
// avoid circling-back upon the source table //
///////////////////////////////////////////////
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType()) && table.getName().equals(possibleValueSource.getTableName()))
{
continue;
}
record.setDisplayValue(joinFieldName, translatePossibleValue(field, record.getValue(joinFieldName)));
}
}
}
}
catch(Exception e)
{
LOG.warn("Error translating join table possible values", e);
}
}
}
}
}
/*******************************************************************************
**
** Translate a list of ids to a list of possible values (e.g., w/ rendered values)
*******************************************************************************/
String translatePossibleValue(QFieldMetaData field, Serializable value)
public List<QPossibleValue<?>> buildTranslatedPossibleValueList(QPossibleValueSource possibleValueSource, List<Serializable> ids)
{
if(ids == null)
{
return (null);
}
if(ids.isEmpty())
{
return (new ArrayList<>());
}
List<QPossibleValue<?>> rs = new ArrayList<>();
if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), ids);
}
for(Serializable id : ids)
{
String translated = translatePossibleValue(possibleValueSource, id);
rs.add(new QPossibleValue<>(id, translated));
}
return (rs);
}
/*******************************************************************************
** For a given field and (raw/id) value, get the translated (string) value.
*******************************************************************************/
public String translatePossibleValue(QFieldMetaData field, Serializable value)
{
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
if(possibleValueSource == null)
@ -122,6 +218,31 @@ public class QPossibleValueTranslator
return (null);
}
try
{
if(field.getType().equals(QFieldType.INTEGER) && !(value instanceof Integer))
{
value = ValueUtils.getValueAsInteger(value);
}
}
catch(QValueException e)
{
LOG.info("Error translating possible value raw value...");
///////////////////////////
// leave value as it was //
///////////////////////////
}
return translatePossibleValue(possibleValueSource, value);
}
/*******************************************************************************
** For a given PossibleValueSource and (raw/id) value, get the translated (string) value.
*******************************************************************************/
String translatePossibleValue(QPossibleValueSource possibleValueSource, Serializable value)
{
String resultValue = null;
if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM))
{
@ -129,15 +250,15 @@ public class QPossibleValueTranslator
}
else if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
resultValue = translatePossibleValueTable(field, value, possibleValueSource);
resultValue = translatePossibleValueTable(value, possibleValueSource);
}
else if(possibleValueSource.getType().equals(QPossibleValueSourceType.CUSTOM))
{
resultValue = translatePossibleValueCustom(field, value, possibleValueSource);
resultValue = translatePossibleValueCustom(value, possibleValueSource);
}
else
{
LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "] on field [" + field.getName() + "]");
LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "]");
}
if(resultValue == null)
@ -151,7 +272,7 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
** do translation for an enum-type PVS
*******************************************************************************/
private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource)
{
@ -169,9 +290,9 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
** do translation for a table-type PVS
*******************************************************************************/
private String translatePossibleValueTable(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource)
String translatePossibleValueTable(Serializable value, QPossibleValueSource possibleValueSource)
{
/////////////////////////////////
// null input gets null output //
@ -184,8 +305,7 @@ public class QPossibleValueTranslator
//////////////////////////////////////////////////////////////
// look for cached value - if it's missing, call the primer //
//////////////////////////////////////////////////////////////
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
Map<Serializable, String> cacheForPvs = possibleValueCache.get(possibleValueSource.getName());
Map<Serializable, String> cacheForPvs = possibleValueCache.computeIfAbsent(possibleValueSource.getName(), x -> new HashMap<>());
if(!cacheForPvs.containsKey(value))
{
primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), List.of(value));
@ -197,9 +317,9 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
** do translation for a custom-type PVS
*******************************************************************************/
private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource)
private String translatePossibleValueCustom(Serializable value, QPossibleValueSource possibleValueSource)
{
try
{
@ -208,7 +328,7 @@ public class QPossibleValueTranslator
}
catch(Exception e)
{
LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e);
LOG.warn("Error sending [" + value + "] for through custom code for PVS [" + possibleValueSource.getName() + "]", e);
}
return (null);
@ -269,21 +389,31 @@ public class QPossibleValueTranslator
/*******************************************************************************
** prime the cache (e.g., by doing bulk-queries) for table-based PVS's
**
** @param table the table that the records are from
*
* @param table the table that the records are from
** @param records the records that have the possible value id's (e.g., foreign keys)
* @param queryJoins joins that were used as part of the query that led to the records.
* @param limitedToFieldNames set of names that are the only fields that get translated (null means all fields).
*******************************************************************************/
void primePvsCache(QTableMetaData table, List<QRecord> records)
void primePvsCache(QTableMetaData table, List<QRecord> records, List<QueryJoin> queryJoins, Set<String> limitedToFieldNames)
{
ListingHash<String, QFieldMetaData> fieldsByPvsTable = new ListingHash<>();
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is a map of String(tableName - the PVS table) to Pair(String (either "" for main table in a query, or join-table + "."), field (from the table being selected from)) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ListingHash<String, Pair<String, QFieldMetaData>> fieldsByPvsTable = new ListingHash<>();
///////////////////////////////////////////////////////////////////////////////////////
// this is a map of String(tableName - the PVS table) to PossibleValueSource objects //
///////////////////////////////////////////////////////////////////////////////////////
ListingHash<String, QPossibleValueSource> pvsesByTable = new ListingHash<>();
for(QFieldMetaData field : table.getFields().values())
primePvsCacheTableListingHashLoader(table, fieldsByPvsTable, pvsesByTable, "", table.getName(), limitedToFieldNames);
for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins))
{
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
if(queryJoin.getSelect())
{
fieldsByPvsTable.add(possibleValueSource.getTableName(), field);
pvsesByTable.add(possibleValueSource.getTableName(), possibleValueSource);
String aliasOrTableName = Objects.requireNonNullElse(queryJoin.getAlias(), queryJoin.getJoinTable());
primePvsCacheTableListingHashLoader(qInstance.getTable(queryJoin.getJoinTable()), fieldsByPvsTable, pvsesByTable, aliasOrTableName + ".", queryJoin.getJoinTable(), limitedToFieldNames);
}
}
@ -292,15 +422,66 @@ public class QPossibleValueTranslator
Set<Serializable> values = new HashSet<>();
for(QRecord record : records)
{
for(QFieldMetaData field : fieldsByPvsTable.get(tableName))
for(Pair<String, QFieldMetaData> fieldPair : fieldsByPvsTable.get(tableName))
{
values.add(record.getValue(field.getName()));
String fieldName = fieldPair.getA() + fieldPair.getB().getName();
Serializable fieldValue = record.getValue(fieldName);
/////////////////////////////////////////
// ignore null and empty-string values //
/////////////////////////////////////////
if(!StringUtils.hasContent(ValueUtils.getValueAsString(fieldValue)))
{
continue;
}
//////////////////////////////////////
// check if value is already cached //
//////////////////////////////////////
QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0);
Map<Serializable, String> cacheForPvs = possibleValueCache.computeIfAbsent(possibleValueSource.getName(), x -> new HashMap<>());
if(!cacheForPvs.containsKey(fieldValue))
{
values.add(fieldValue);
}
}
}
if(!values.isEmpty())
{
primePvsCache(tableName, pvsesByTable.get(tableName), values);
}
}
}
/*******************************************************************************
** Helper for the primePvsCache method
*******************************************************************************/
private void primePvsCacheTableListingHashLoader(QTableMetaData table, ListingHash<String, Pair<String, QFieldMetaData>> fieldsByPvsTable, ListingHash<String, QPossibleValueSource> pvsesByTable, String fieldNamePrefix, String tableName, Set<String> limitedToFieldNames)
{
for(QFieldMetaData field : table.getFields().values())
{
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
if(limitedToFieldNames != null && !limitedToFieldNames.contains(fieldNamePrefix + field.getName()))
{
LOG.debug("Skipping cache priming for translation of possible value field [" + fieldNamePrefix + field.getName() + "] - it's not in the limitedToFieldNames set.");
continue;
}
fieldsByPvsTable.add(possibleValueSource.getTableName(), Pair.of(fieldNamePrefix, field));
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - optimization we can put the same PVS in this listing hash multiple times... either check for dupes, or change to a set, or something smarter. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
pvsesByTable.add(possibleValueSource.getTableName(), possibleValueSource);
}
}
}
@ -323,18 +504,50 @@ public class QPossibleValueTranslator
for(List<Serializable> page : CollectionUtils.getPages(values, 1000))
{
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setSession(session);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page)));
/////////////////////////////////////////////////////////////////////////////////////////
// this is needed to get record labels, which are what we use here... unclear if best! //
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// when querying for possible values, we do want to generate their display values, which makes record labels, which are usually used as PVS labels //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
queryInput.setShouldGenerateDisplayValues(true);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// also, if this table uses any possible value fields as part of its own record label, then THOSE possible values need translated. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Set<String> possibleValueFieldsToTranslate = new HashSet<>();
for(QPossibleValueSource possibleValueSource : possibleValueSources)
{
if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
for(String recordLabelField : CollectionUtils.nonNullList(table.getRecordLabelFields()))
{
QFieldMetaData field = table.getField(recordLabelField);
if(field.getPossibleValueSourceName() != null)
{
possibleValueFieldsToTranslate.add(field.getName());
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// an earlier version of this code got into stack overflows, so do a "cheap" check for recursion depth too... //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(!possibleValueFieldsToTranslate.isEmpty() && notTooDeep())
{
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setFieldsToTranslatePossibleValues(possibleValueFieldsToTranslate);
}
LOG.debug("Priming PVS cache for [" + page.size() + "] ids from [" + tableName + "] table.");
QueryOutput queryOutput = new QueryAction().execute(queryInput);
///////////////////////////////////////////////////////////////////////////////////
// for all records that were found, put a formatted value into cache foreach PVS //
///////////////////////////////////////////////////////////////////////////////////
for(QRecord record : queryOutput.getRecords())
{
Serializable pkeyValue = record.getValue(primaryKeyField);
@ -344,6 +557,20 @@ public class QPossibleValueTranslator
possibleValueCache.get(possibleValueSource.getName()).put(pkeyValue, formatPossibleValue(possibleValueSource, possibleValue));
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// for all pkeys that were NOT found, put a null value into cache foreach PVS (to avoid re-looking up) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
for(Serializable pkey : page)
{
for(QPossibleValueSource possibleValueSource : possibleValueSources)
{
if(!possibleValueCache.get(possibleValueSource.getName()).containsKey(pkey))
{
possibleValueCache.get(possibleValueSource.getName()).put(pkey, null);
}
}
}
}
}
catch(Exception e)
@ -352,4 +579,24 @@ public class QPossibleValueTranslator
}
}
/*******************************************************************************
** Avoid infinite recursion, for where one field's PVS depends on another's...
** not too smart, just breaks at 5...
*******************************************************************************/
private boolean notTooDeep()
{
int count = 0;
for(StackTraceElement stackTraceElement : new Throwable().getStackTrace())
{
if(stackTraceElement.getMethodName().equals("translatePossibleValuesInRecords"))
{
count++;
}
}
return (count < 5);
}
}

View File

@ -23,14 +23,19 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -39,14 +44,55 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QValueFormatter
{
private static final Logger LOG = LogManager.getLogger(QValueFormatter.class);
private static final QLogger LOG = QLogger.getLogger(QValueFormatter.class);
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mm a");
private static DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/*******************************************************************************
**
** For a field, and its value, apply the field's displayFormat.
*******************************************************************************/
public String formatValue(QFieldMetaData field, Serializable value)
public static String formatValue(QFieldMetaData field, Serializable value)
{
if(QFieldType.BOOLEAN.equals(field.getType()))
{
Boolean b = ValueUtils.getValueAsBoolean(value);
if(b == null)
{
return (null);
}
else if(b)
{
return ("Yes");
}
else
{
return ("No");
}
}
return (formatValue(field.getDisplayFormat(), field.getName(), value));
}
/*******************************************************************************
** For a display format string (e.g., %d), and a value, apply the displayFormat.
*******************************************************************************/
public static String formatValue(String displayFormat, Serializable value)
{
return (formatValue(displayFormat, "", value));
}
/*******************************************************************************
** 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)
{
//////////////////////////////////
// null values get null results //
@ -59,11 +105,11 @@ public class QValueFormatter
////////////////////////////////////////////////////////
// if the field has a display format, try to apply it //
////////////////////////////////////////////////////////
if(StringUtils.hasContent(field.getDisplayFormat()))
if(StringUtils.hasContent(displayFormat))
{
try
{
return (field.getDisplayFormat().formatted(value));
return (displayFormat.formatted(value));
}
catch(Exception e)
{
@ -72,20 +118,24 @@ public class QValueFormatter
// todo - revisit if we actually want this - or - if you should get an error if you mis-configure your table this way (ideally during validation!)
if(e.getMessage().equals("f != java.lang.Integer"))
{
return formatValue(field, ValueUtils.getValueAsBigDecimal(value));
return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value));
}
else if(e.getMessage().equals("f != java.lang.String"))
{
return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value));
}
else if(e.getMessage().equals("d != java.math.BigDecimal"))
{
return formatValue(field, ValueUtils.getValueAsInteger(value));
return formatValue(displayFormat, ValueUtils.getValueAsInteger(value));
}
else
{
LOG.warn("Error formatting value [" + value + "] for field [" + field.getName() + "] with format [" + field.getDisplayFormat() + "]: " + e.getMessage());
LOG.warn("Error formatting value [" + value + "] for field [" + fieldName + "] with format [" + displayFormat + "]: " + e.getMessage());
}
}
catch(Exception e2)
{
LOG.warn("Caught secondary exception trying to convert type on field [" + field.getName() + "] for formatting", e);
LOG.warn("Caught secondary exception trying to convert type on field [" + fieldName + "] for formatting", e);
}
}
}
@ -98,39 +148,93 @@ public class QValueFormatter
/*******************************************************************************
**
*******************************************************************************/
public static String formatDate(LocalDate date)
{
return (dateFormatter.format(date));
}
/*******************************************************************************
**
*******************************************************************************/
public static String formatDateTime(LocalDateTime dateTime)
{
return (dateTimeFormatter.format(dateTime));
}
/*******************************************************************************
** Make a string from a table's recordLabelFormat and fields, for a given record.
*******************************************************************************/
public String formatRecordLabel(QTableMetaData table, QRecord record)
public static String formatRecordLabel(QTableMetaData table, QRecord record)
{
if(!StringUtils.hasContent(table.getRecordLabelFormat()))
{
return (formatRecordLabelExceptionalCases(table, record));
}
///////////////////////////////////////////////////////////////////////
// get list of values, then pass them to the string formatter method //
///////////////////////////////////////////////////////////////////////
try
{
List<Serializable> values = table.getRecordLabelFields().stream()
.map(record::getValue)
.map(v -> v == null ? "" : v)
.toList();
return (table.getRecordLabelFormat().formatted(values.toArray()));
return formatStringWithFields(table.getRecordLabelFormat(), table.getRecordLabelFields(), record.getDisplayValues(), record.getValues());
}
catch(Exception e)
{
LOG.debug("Error formatting record label", e);
return (formatRecordLabelExceptionalCases(table, record));
}
}
/*******************************************************************************
** For a given format string, and a list of fields, look in displayValueMap and
** rawValueMap to get the values to apply to the format.
*******************************************************************************/
private static String formatStringWithFields(String formatString, List<String> formatFields, Map<String, String> displayValueMap, Map<String, Serializable> rawValueMap)
{
List<Serializable> values = formatFields.stream()
.map(fieldName ->
{
///////////////////////////////////////////////////////////////////////////
// if there's a display value set, then use it. Else, use the raw value //
///////////////////////////////////////////////////////////////////////////
String displayValue = displayValueMap.get(fieldName);
if(displayValue != null)
{
return (displayValue);
}
return rawValueMap.get(fieldName);
})
.map(v -> v == null ? "" : v)
.toList();
return (formatString.formatted(values.toArray()));
}
/*******************************************************************************
** For a given format string, and a list of values, apply the format. Note, null
** values in the list become "".
*******************************************************************************/
public static String formatStringWithValues(String formatString, List<String> formatValues)
{
List<String> values = formatValues.stream()
.map(v -> v == null ? "" : v)
.toList();
return (formatString.formatted(values.toArray()));
}
/*******************************************************************************
** Deal with non-happy-path cases for making a record label.
*******************************************************************************/
private String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record)
private static String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record)
{
///////////////////////////////////////////////////////////////////////////////////////
// if there's no record label format, then just return the primary key display value //
@ -158,7 +262,7 @@ public class QValueFormatter
/*******************************************************************************
** For a list of records, set their recordLabels and display values
*******************************************************************************/
public void setDisplayValuesInRecords(QTableMetaData table, List<QRecord> records)
public static void setDisplayValuesInRecords(QTableMetaData table, List<QRecord> records)
{
if(records == null)
{
@ -167,14 +271,26 @@ public class QValueFormatter
for(QRecord record : records)
{
for(QFieldMetaData field : table.getFields().values())
{
String formattedValue = formatValue(field, record.getValue(field.getName()));
record.setDisplayValue(field.getName(), formattedValue);
}
setDisplayValuesInRecord(table.getFields().values(), record);
record.setRecordLabel(formatRecordLabel(table, record));
}
}
/*******************************************************************************
** For a list of records, set their display values
*******************************************************************************/
public static void setDisplayValuesInRecord(Collection<QFieldMetaData> fields, QRecord record)
{
for(QFieldMetaData field : fields)
{
if(record.getDisplayValue(field.getName()) == null)
{
String formattedValue = formatValue(field, record.getValue(field.getName()));
record.setDisplayValue(field.getName(), formattedValue);
}
}
}
}

View File

@ -0,0 +1,290 @@
/*
* 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.values;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
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;
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.actions.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
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;
/*******************************************************************************
** Class responsible for looking up possible-values for fields/records and
** make them into display values.
*******************************************************************************/
public class SearchPossibleValueSourceAction
{
private static final QLogger LOG = QLogger.getLogger(SearchPossibleValueSourceAction.class);
private QPossibleValueTranslator possibleValueTranslator;
/*******************************************************************************
**
*******************************************************************************/
public SearchPossibleValueSourceOutput execute(SearchPossibleValueSourceInput input) throws QException
{
QInstance qInstance = input.getInstance();
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());
SearchPossibleValueSourceOutput output = null;
if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM))
{
output = searchPossibleValueEnum(input, possibleValueSource);
}
else if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
output = searchPossibleValueTable(input, possibleValueSource);
}
else if(possibleValueSource.getType().equals(QPossibleValueSourceType.CUSTOM))
{
output = searchPossibleValueCustom(input, possibleValueSource);
}
else
{
LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "]");
}
return (output);
}
/*******************************************************************************
**
*******************************************************************************/
private SearchPossibleValueSourceOutput searchPossibleValueEnum(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource)
{
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;
}
}
if(match)
{
matchingIds.add((Serializable) possibleValue.getId());
}
// todo - skip & limit?
// todo - default filter
}
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, matchingIds);
output.setResults(qPossibleValues);
return (output);
}
/*******************************************************************************
** 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.
*******************************************************************************/
private List<Object> convertInputIdsToEnumIdType(QPossibleValueSource possibleValueSource, List<Serializable> inputIdList)
{
List<Object> rs = new ArrayList<>();
if(CollectionUtils.nullSafeIsEmpty(inputIdList))
{
return (rs);
}
Object anIdFromTheEnum = possibleValueSource.getEnumValues().get(0).getId();
if(anIdFromTheEnum instanceof Integer)
{
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());
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private SearchPossibleValueSourceOutput searchPossibleValueTable(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource) throws QException
{
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(possibleValueSource.getTableName());
QTableMetaData table = input.getInstance().getTable(possibleValueSource.getTableName());
QQueryFilter queryFilter = new QQueryFilter();
queryFilter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
if(input.getIdList() != null)
{
queryFilter.addCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, input.getIdList()));
}
else
{
if(StringUtils.hasContent(input.getSearchTerm()))
{
for(String valueField : possibleValueSource.getSearchFields())
{
QFieldMetaData field = table.getField(valueField);
if(field.getType().equals(QFieldType.STRING))
{
queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.STARTS_WITH, List.of(input.getSearchTerm())));
}
else if(field.getType().equals(QFieldType.DATE) || field.getType().equals(QFieldType.DATE_TIME))
{
LOG.debug("Not querying PVS [" + possibleValueSource.getName() + "] on date field [" + field.getName() + "]");
// todo - what? queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.STARTS_WITH, List.of(input.getSearchTerm())));
}
else
{
try
{
Integer valueAsInteger = ValueUtils.getValueAsInteger(input.getSearchTerm());
if(valueAsInteger != null)
{
queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.EQUALS, List.of(valueAsInteger)));
}
}
catch(Exception e)
{
////////////////////////////////////////////////////////
// write a FALSE criteria if the value isn't a number //
////////////////////////////////////////////////////////
queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.IN, List.of()));
}
}
}
}
}
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
// todo - skip & limit as params
queryInput.setLimit(250);
///////////////////////////////////////////////////////////////////////////////////////////////////////
// if given a default filter, make it the 'top level' filter and the one we just created a subfilter //
///////////////////////////////////////////////////////////////////////////////////////////////////////
if(input.getDefaultQueryFilter() != null)
{
input.getDefaultQueryFilter().addSubFilter(queryFilter);
queryFilter = input.getDefaultQueryFilter();
}
queryInput.setFilter(queryFilter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList();
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, ids);
output.setResults(qPossibleValues);
return (output);
}
/*******************************************************************************
**
*******************************************************************************/
private SearchPossibleValueSourceOutput searchPossibleValueCustom(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource)
{
try
{
// QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
// return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value)));
}
catch(Exception e)
{
// LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e);
}
throw new NotImplementedException("Not impleemnted");
// return (null);
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.values;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Utility class to apply value behaviors to records.
*******************************************************************************/
public class ValueBehaviorApplier
{
/*******************************************************************************
**
*******************************************************************************/
public static void applyFieldBehaviors(QInstance instance, QTableMetaData table, List<QRecord> recordList)
{
for(QFieldMetaData field : table.getFields().values())
{
String fieldName = field.getName();
if(field.getType().equals(QFieldType.STRING) && field.getMaxLength() != null)
{
applyValueTooLongBehavior(instance, recordList, field, fieldName);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void applyValueTooLongBehavior(QInstance instance, List<QRecord> recordList, QFieldMetaData field, String fieldName)
{
ValueTooLongBehavior valueTooLongBehavior = field.getBehavior(instance, ValueTooLongBehavior.class);
////////////////////////////////////////////////////////////////////////////////////////////////////
// don't process PASS_THROUGH - so we don't have to iterate over the whole record list to do noop //
////////////////////////////////////////////////////////////////////////////////////////////////////
if(valueTooLongBehavior != null && !valueTooLongBehavior.equals(ValueTooLongBehavior.PASS_THROUGH))
{
for(QRecord record : recordList)
{
String value = record.getValueString(fieldName);
if(value != null && value.length() > field.getMaxLength())
{
switch(valueTooLongBehavior)
{
case TRUNCATE -> record.setValue(fieldName, StringUtils.safeTruncate(value, field.getMaxLength()));
case TRUNCATE_ELLIPSIS -> record.setValue(fieldName, StringUtils.safeTruncate(value, field.getMaxLength(), "..."));
case ERROR -> record.addError("The value for " + field.getLabel() + " is too long (max allowed length=" + field.getMaxLength() + ")");
case PASS_THROUGH ->
{
}
default -> throw new IllegalStateException("Unexpected valueTooLongBehavior: " + valueTooLongBehavior);
}
}
}
}
}
}

View File

@ -26,6 +26,7 @@ import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@ -62,8 +63,7 @@ public class CsvToQRecordAdapter
*******************************************************************************/
public void buildRecordsFromCsv(RecordPipe recordPipe, String csv, QTableMetaData table, AbstractQFieldMapping<?> mapping, Consumer<QRecord> recordCustomizer)
{
this.recordPipe = recordPipe;
doBuildRecordsFromCsv(csv, table, mapping, recordCustomizer);
buildRecordsFromCsv(new InputWrapper().withRecordPipe(recordPipe).withCsv(csv).withTable(table).withMapping(mapping).withRecordCustomizer(recordCustomizer));
}
@ -75,8 +75,7 @@ public class CsvToQRecordAdapter
*******************************************************************************/
public List<QRecord> buildRecordsFromCsv(String csv, QTableMetaData table, AbstractQFieldMapping<?> mapping)
{
this.recordList = new ArrayList<>();
doBuildRecordsFromCsv(csv, table, mapping, null);
buildRecordsFromCsv(new InputWrapper().withCsv(csv).withTable(table).withMapping(mapping));
return (recordList);
}
@ -88,13 +87,29 @@ public class CsvToQRecordAdapter
**
** todo - meta-data validation, type handling
*******************************************************************************/
public void doBuildRecordsFromCsv(String csv, QTableMetaData table, AbstractQFieldMapping<?> mapping, Consumer<QRecord> recordCustomizer)
public void buildRecordsFromCsv(InputWrapper inputWrapper)
{
String csv = inputWrapper.getCsv();
AbstractQFieldMapping<?> mapping = inputWrapper.getMapping();
Consumer<QRecord> recordCustomizer = inputWrapper.getRecordCustomizer();
QTableMetaData table = inputWrapper.getTable();
Integer limit = inputWrapper.getLimit();
if(!StringUtils.hasContent(csv))
{
throw (new IllegalArgumentException("Empty csv value was provided."));
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// if caller supplied a record pipe, use it -- but if it's null, then create a recordList to populate. //
// see addRecord method for usage. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
this.recordPipe = inputWrapper.getRecordPipe();
if(this.recordPipe == null)
{
this.recordList = new ArrayList<>();
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// once, from a DOS csv file (that had come from Excel), we had a "" character (FEFF, Byte-order marker) at the start of a //
// CSV, which caused our first header to not match... So, let us strip away any FEFF or FFFE's at the start of CSV strings. //
@ -120,16 +135,20 @@ public class CsvToQRecordAdapter
List<String> headers = csvParser.getHeaderNames();
headers = makeHeadersUnique(headers);
List<CSVRecord> csvRecords = csvParser.getRecords();
for(CSVRecord csvRecord : csvRecords)
Iterator<CSVRecord> csvIterator = csvParser.iterator();
int recordCount = 0;
while(csvIterator.hasNext())
{
CSVRecord csvRecord = csvIterator.next();
//////////////////////////////////////////////////////////////////
// put values from the CSV record into a map of header -> value //
//////////////////////////////////////////////////////////////////
Map<String, String> csvValues = new HashMap<>();
for(int i = 0; i < headers.size() && i < csvRecord.size(); i++)
{
csvValues.put(headers.get(i), csvRecord.get(i));
String header = adjustHeaderCase(headers.get(i), inputWrapper);
csvValues.put(header, csvRecord.get(i));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -139,11 +158,18 @@ public class CsvToQRecordAdapter
for(QFieldMetaData field : table.getFields().values())
{
String fieldSource = mapping == null ? field.getName() : String.valueOf(mapping.getFieldSource(field.getName()));
fieldSource = adjustHeaderCase(fieldSource, inputWrapper);
qRecord.setValue(field.getName(), csvValues.get(fieldSource));
}
runRecordCustomizer(recordCustomizer, qRecord);
addRecord(qRecord);
recordCount++;
if(limit != null && recordCount > limit)
{
break;
}
}
}
else if(AbstractQFieldMapping.SourceType.INDEX.equals(mapping.getSourceType()))
@ -155,9 +181,12 @@ public class CsvToQRecordAdapter
CSVFormat.DEFAULT
.withTrim());
List<CSVRecord> csvRecords = csvParser.getRecords();
for(CSVRecord csvRecord : csvRecords)
Iterator<CSVRecord> csvIterator = csvParser.iterator();
int recordCount = 0;
while(csvIterator.hasNext())
{
CSVRecord csvRecord = csvIterator.next();
/////////////////////////////////////////////////////////////////
// put values from the CSV record into a map of index -> value //
/////////////////////////////////////////////////////////////////
@ -180,6 +209,12 @@ public class CsvToQRecordAdapter
runRecordCustomizer(recordCustomizer, qRecord);
addRecord(qRecord);
recordCount++;
if(limit != null && recordCount > limit)
{
break;
}
}
}
else
@ -195,6 +230,20 @@ public class CsvToQRecordAdapter
/*******************************************************************************
**
*******************************************************************************/
private String adjustHeaderCase(String s, InputWrapper inputWrapper)
{
if(inputWrapper.caseSensitiveHeaders)
{
return (s);
}
return (s.toLowerCase());
}
/*******************************************************************************
**
*******************************************************************************/
@ -261,4 +310,277 @@ public class CsvToQRecordAdapter
}
}
/*******************************************************************************
** Getter for recordList - note - only is valid if you don't supply a pipe in
** the input. If you do supply a pipe, then you get an exception if you call here!
**
*******************************************************************************/
public List<QRecord> getRecordList()
{
if(recordPipe != null)
{
throw (new IllegalStateException("getRecordList called on a CSVToQRecordAdapter that ran with a recordPipe."));
}
return recordList;
}
/*******************************************************************************
**
*******************************************************************************/
public static class InputWrapper
{
private RecordPipe recordPipe;
private String csv;
private QTableMetaData table;
private AbstractQFieldMapping<?> mapping;
private Consumer<QRecord> recordCustomizer;
private Integer limit;
private boolean caseSensitiveHeaders = false;
/*******************************************************************************
** Getter for recordPipe
**
*******************************************************************************/
public RecordPipe getRecordPipe()
{
return recordPipe;
}
/*******************************************************************************
** Setter for recordPipe
**
*******************************************************************************/
public void setRecordPipe(RecordPipe recordPipe)
{
this.recordPipe = recordPipe;
}
/*******************************************************************************
** Fluent setter for recordPipe
**
*******************************************************************************/
public InputWrapper withRecordPipe(RecordPipe recordPipe)
{
this.recordPipe = recordPipe;
return (this);
}
/*******************************************************************************
** Getter for csv
**
*******************************************************************************/
public String getCsv()
{
return csv;
}
/*******************************************************************************
** Setter for csv
**
*******************************************************************************/
public void setCsv(String csv)
{
this.csv = csv;
}
/*******************************************************************************
** Fluent setter for csv
**
*******************************************************************************/
public InputWrapper withCsv(String csv)
{
this.csv = csv;
return (this);
}
/*******************************************************************************
** Getter for table
**
*******************************************************************************/
public QTableMetaData getTable()
{
return table;
}
/*******************************************************************************
** Setter for table
**
*******************************************************************************/
public void setTable(QTableMetaData table)
{
this.table = table;
}
/*******************************************************************************
** Fluent setter for table
**
*******************************************************************************/
public InputWrapper withTable(QTableMetaData table)
{
this.table = table;
return (this);
}
/*******************************************************************************
** Getter for mapping
**
*******************************************************************************/
public AbstractQFieldMapping<?> getMapping()
{
return mapping;
}
/*******************************************************************************
** Setter for mapping
**
*******************************************************************************/
public void setMapping(AbstractQFieldMapping<?> mapping)
{
this.mapping = mapping;
}
/*******************************************************************************
** Fluent setter for mapping
**
*******************************************************************************/
public InputWrapper withMapping(AbstractQFieldMapping<?> mapping)
{
this.mapping = mapping;
return (this);
}
/*******************************************************************************
** Getter for recordCustomizer
**
*******************************************************************************/
public Consumer<QRecord> getRecordCustomizer()
{
return recordCustomizer;
}
/*******************************************************************************
** Setter for recordCustomizer
**
*******************************************************************************/
public void setRecordCustomizer(Consumer<QRecord> recordCustomizer)
{
this.recordCustomizer = recordCustomizer;
}
/*******************************************************************************
** Fluent setter for recordCustomizer
**
*******************************************************************************/
public InputWrapper withRecordCustomizer(Consumer<QRecord> recordCustomizer)
{
this.recordCustomizer = recordCustomizer;
return (this);
}
/*******************************************************************************
** Getter for limit
**
*******************************************************************************/
public Integer getLimit()
{
return limit;
}
/*******************************************************************************
** Setter for limit
**
*******************************************************************************/
public void setLimit(Integer limit)
{
this.limit = limit;
}
/*******************************************************************************
** Fluent setter for limit
**
*******************************************************************************/
public InputWrapper withLimit(Integer limit)
{
this.limit = limit;
return (this);
}
/*******************************************************************************
** Getter for caseSensitiveHeaders
**
*******************************************************************************/
public boolean getCaseSensitiveHeaders()
{
return caseSensitiveHeaders;
}
/*******************************************************************************
** Setter for caseSensitiveHeaders
**
*******************************************************************************/
public void setCaseSensitiveHeaders(boolean caseSensitiveHeaders)
{
this.caseSensitiveHeaders = caseSensitiveHeaders;
}
/*******************************************************************************
** Fluent setter for caseSensitiveHeaders
**
*******************************************************************************/
public InputWrapper withCaseSensitiveHeaders(boolean caseSensitiveHeaders)
{
this.caseSensitiveHeaders = caseSensitiveHeaders;
return (this);
}
}
}

View File

@ -0,0 +1,38 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.context;
import java.util.Stack;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
** record containing the values managed by QContext.
*******************************************************************************/
public record CapturedContext(QInstance qInstance, QSession qSession, QBackendTransaction qBackendTransaction, Stack<AbstractActionInput> actionStack)
{
}

View File

@ -0,0 +1,246 @@
/*
* 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.context;
import java.util.Stack;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
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.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
** A collection of thread-local variables, to define the current context of the
** QQQ code that is running. e.g., what QInstance is being used, what QSession
** is active, etc.
*******************************************************************************/
public class QContext
{
private static final QLogger LOG = QLogger.getLogger(QContext.class);
private static ThreadLocal<QInstance> qInstanceThreadLocal = new ThreadLocal<>();
private static ThreadLocal<QSession> qSessionThreadLocal = new ThreadLocal<>();
private static ThreadLocal<QBackendTransaction> qBackendTransactionThreadLocal = new ThreadLocal<>();
private static ThreadLocal<Stack<AbstractActionInput>> actionStackThreadLocal = new ThreadLocal<>();
/*******************************************************************************
** private constructor - class is not meant to be instantiated.
*******************************************************************************/
private QContext()
{
}
/*******************************************************************************
** Most common method to set or init the context - e.g., set the current thread
** with a QInstance and QSession.
*******************************************************************************/
public static void init(QInstance qInstance, QSession qSession)
{
init(qInstance, qSession, null, null);
}
/*******************************************************************************
** Full flavor init method - also take a transaction and action input (to seed the stack).
*******************************************************************************/
public static void init(QInstance qInstance, QSession qSession, QBackendTransaction transaction, AbstractActionInput actionInput)
{
qInstanceThreadLocal.set(qInstance);
qSessionThreadLocal.set(qSession);
qBackendTransactionThreadLocal.set(transaction);
actionStackThreadLocal.set(new Stack<>());
if(actionInput != null)
{
actionStackThreadLocal.get().add(actionInput);
}
if(!qInstance.getHasBeenValidated())
{
try
{
new QInstanceValidator().validate(qInstance);
}
catch(QInstanceValidationException e)
{
LOG.warn(e);
throw (new IllegalArgumentException("QInstance failed validation" + e.getMessage(), e));
}
}
}
/*******************************************************************************
** Init a new thread with the context captured from a different thread. e.g.,
** when starting some async task.
*******************************************************************************/
public static void init(CapturedContext capturedContext)
{
init(capturedContext.qInstance(), capturedContext.qSession(), capturedContext.qBackendTransaction(), null);
actionStackThreadLocal.set(capturedContext.actionStack());
}
/*******************************************************************************
** Capture all values from the current thread - meant to be used with the init
** overload that takes a CapturedContext, for setting up a child thread.
*******************************************************************************/
public static CapturedContext capture()
{
return (new CapturedContext(getQInstance(), getQSession(), getQBackendTransaction(), getActionStack()));
}
/*******************************************************************************
** Clear all values in the current thread.
*******************************************************************************/
public static void clear()
{
qInstanceThreadLocal.remove();
qSessionThreadLocal.remove();
qBackendTransactionThreadLocal.remove();
actionStackThreadLocal.remove();
}
/*******************************************************************************
**
*******************************************************************************/
public static QInstance getQInstance()
{
return (qInstanceThreadLocal.get());
}
/*******************************************************************************
**
*******************************************************************************/
public static QSession getQSession()
{
return (qSessionThreadLocal.get());
}
/*******************************************************************************
**
*******************************************************************************/
public static QBackendTransaction getQBackendTransaction()
{
return (qBackendTransactionThreadLocal.get());
}
/*******************************************************************************
**
*******************************************************************************/
public static Stack<AbstractActionInput> getActionStack()
{
return (actionStackThreadLocal.get());
}
/*******************************************************************************
**
*******************************************************************************/
public static void pushAction(AbstractActionInput action)
{
if(actionStackThreadLocal.get() == null)
{
actionStackThreadLocal.set(new Stack<>());
}
actionStackThreadLocal.get().push(action);
}
/*******************************************************************************
**
*******************************************************************************/
public static void popAction()
{
try
{
actionStackThreadLocal.get().pop();
}
catch(Exception e)
{
LOG.debug("Error popping action stack", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void setQInstance(QInstance qInstance)
{
qInstanceThreadLocal.set(qInstance);
}
/*******************************************************************************
**
*******************************************************************************/
public static void setQSession(QSession qSession)
{
qSessionThreadLocal.set(qSession);
}
/*******************************************************************************
**
*******************************************************************************/
public static void setTransaction(QBackendTransaction transaction)
{
qBackendTransactionThreadLocal.set(transaction);
}
/*******************************************************************************
**
*******************************************************************************/
public static void clearTransaction()
{
qBackendTransactionThreadLocal.remove();
}
}

Some files were not shown because too many files have changed in this diff Show More